diff --git a/.changeset/allow-keyless-indexing.md b/.changeset/allow-keyless-indexing.md deleted file mode 100644 index c60fa891408..00000000000 --- a/.changeset/allow-keyless-indexing.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@kilocode/cli": patch -"@kilocode/kilo-indexing": patch -"kilo-code": patch ---- - -Support unauthenticated OpenAI-compatible endpoints for codebase indexing without requiring a placeholder API key. diff --git a/.changeset/animate-tool-card-reveals.md b/.changeset/animate-tool-card-reveals.md new file mode 100644 index 00000000000..a273d7ae45e --- /dev/null +++ b/.changeset/animate-tool-card-reveals.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Animate tool-call and reasoning details when expanding and collapsing. diff --git a/.changeset/bound-snapshot-turns.md b/.changeset/bound-snapshot-turns.md new file mode 100644 index 00000000000..7a2cb9871eb --- /dev/null +++ b/.changeset/bound-snapshot-turns.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep turns responsive when snapshot infrastructure stalls and prevent transient snapshot progress from appearing in forked sessions. diff --git a/.changeset/bright-notebooks-complete.md b/.changeset/bright-notebooks-complete.md new file mode 100644 index 00000000000..afe6eb43733 --- /dev/null +++ b/.changeset/bright-notebooks-complete.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Enable autocomplete in Jupyter notebooks. diff --git a/.changeset/calm-models-reason.md b/.changeset/calm-models-reason.md deleted file mode 100644 index 49fc92940fe..00000000000 --- a/.changeset/calm-models-reason.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Support multiple provider APIs, adaptive thinking, split reasoning, and output effort variants for custom providers. diff --git a/.changeset/calm-powershell-alerts.md b/.changeset/calm-powershell-alerts.md new file mode 100644 index 00000000000..ac702a7abc8 --- /dev/null +++ b/.changeset/calm-powershell-alerts.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Run Windows PowerShell tool commands without `-EncodedCommand` to reduce antivirus false positives. diff --git a/.changeset/calm-snapshot-spinner.md b/.changeset/calm-snapshot-spinner.md deleted file mode 100644 index b303043b2d3..00000000000 --- a/.changeset/calm-snapshot-spinner.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent snapshot initialization progress from blocking conversations after the slow repository prompt. diff --git a/.changeset/chat-fim-fallback.md b/.changeset/chat-fim-fallback.md new file mode 100644 index 00000000000..73ff08611c5 --- /dev/null +++ b/.changeset/chat-fim-fallback.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/kilo-gateway": patch +--- + +Use the matching FIM model for chat autocomplete when Next Edit is selected. diff --git a/.changeset/clear-rate-limit-errors.md b/.changeset/clear-rate-limit-errors.md deleted file mode 100644 index 7feca1af9cd..00000000000 --- a/.changeset/clear-rate-limit-errors.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Show a clear, retryable provider rate-limit error instead of raw response JSON in chat. diff --git a/.changeset/close-other-tabs.md b/.changeset/close-other-tabs.md deleted file mode 100644 index a35c8e3ca6b..00000000000 --- a/.changeset/close-other-tabs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Support closing other Agent Manager tabs from the tab context menu. diff --git a/.changeset/close-read-tui-news.md b/.changeset/close-read-tui-news.md deleted file mode 100644 index ce1fd8845cd..00000000000 --- a/.changeset/close-read-tui-news.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Hide TUI news after they have been opened and add a button to close the news dialog. diff --git a/.changeset/code-edit-block-display.md b/.changeset/code-edit-block-display.md deleted file mode 100644 index 60ae7b9fa70..00000000000 --- a/.changeset/code-edit-block-display.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Support opening code edit and diff blocks expanded by default in the VS Code chat. diff --git a/.changeset/copy-command-scope.md b/.changeset/copy-command-scope.md deleted file mode 100644 index 5ec09c4084f..00000000000 --- a/.changeset/copy-command-scope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Make `/copy` copy the latest agent response and use `/copy-session` for session transcripts. diff --git a/.changeset/create-plan-dir.md b/.changeset/create-plan-dir.md new file mode 100644 index 00000000000..4f8def38d8a --- /dev/null +++ b/.changeset/create-plan-dir.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Create the default `.kilo/plans` directory automatically when Plan mode starts. diff --git a/.changeset/fast-monkeys-smoke.md b/.changeset/fast-monkeys-smoke.md deleted file mode 100644 index 89693843665..00000000000 --- a/.changeset/fast-monkeys-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Always deny tool calls for title, summarize, and compaction diff --git a/.changeset/fix-diff-viewer-corners.md b/.changeset/fix-diff-viewer-corners.md deleted file mode 100644 index ed8c1520ac3..00000000000 --- a/.changeset/fix-diff-viewer-corners.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Remove stray rounded corners between files in the Changes diff viewer. diff --git a/.changeset/fix-jetbrains-provider-settings.md b/.changeset/fix-jetbrains-provider-settings.md deleted file mode 100644 index 1032a7c6b5b..00000000000 --- a/.changeset/fix-jetbrains-provider-settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix JetBrains provider settings after OAuth/connect actions by waiting through transient backend reloads and allowing longer OAuth exchanges. diff --git a/.changeset/fix-kilo-npm-upgrade.md b/.changeset/fix-kilo-npm-upgrade.md deleted file mode 100644 index 307a40eb8f5..00000000000 --- a/.changeset/fix-kilo-npm-upgrade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Fix upgrades to resolve Kilo CLI packages and releases instead of OpenCode packages and versions. diff --git a/.changeset/fix-llm-user-agent.md b/.changeset/fix-llm-user-agent.md deleted file mode 100644 index f5609c9ee41..00000000000 --- a/.changeset/fix-llm-user-agent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Identify Kilo in provider request user-agent headers instead of OpenCode. diff --git a/.changeset/fix-model-picker-navigation.md b/.changeset/fix-model-picker-navigation.md deleted file mode 100644 index d026b0382bc..00000000000 --- a/.changeset/fix-model-picker-navigation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Improve screen reader navigation and provider group controls in the model picker. diff --git a/.changeset/fix-session-scope-toggle.md b/.changeset/fix-session-scope-toggle.md deleted file mode 100644 index 38396df275e..00000000000 --- a/.changeset/fix-session-scope-toggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Show current-worktree sessions by default in the TUI sessions dialog and keep all/current scope toggling working when a scope has no sessions. diff --git a/.changeset/hide-prompt-training-models.md b/.changeset/hide-prompt-training-models.md deleted file mode 100644 index 6221e66b3cd..00000000000 --- a/.changeset/hide-prompt-training-models.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/cli": patch ---- - -Add a shared model setting to hide Kilo Gateway models that may train on your prompts across Kilo clients. diff --git a/.changeset/highlight-changed-characters.md b/.changeset/highlight-changed-characters.md deleted file mode 100644 index c53dcde1074..00000000000 --- a/.changeset/highlight-changed-characters.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Highlight the exact changed characters within modified diff lines. diff --git a/.changeset/highlight-jetbrains-shell-commands.md b/.changeset/highlight-jetbrains-shell-commands.md deleted file mode 100644 index 394ac4ea35e..00000000000 --- a/.changeset/highlight-jetbrains-shell-commands.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Highlight shell tool commands in JetBrains chat transcripts. diff --git a/.changeset/ime-enter-composition-guard.md b/.changeset/ime-enter-composition-guard.md deleted file mode 100644 index f4e0e871716..00000000000 --- a/.changeset/ime-enter-composition-guard.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Ignore Enter while IME composition is active (including Windows virtual-key 229) so composed text is not accidentally submitted from chat and sidebar inputs. diff --git a/.changeset/improve-jetbrains-markdown.md b/.changeset/improve-jetbrains-markdown.md deleted file mode 100644 index ff1a1845e0b..00000000000 --- a/.changeset/improve-jetbrains-markdown.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Improve markdown readability in JetBrains chat transcripts. diff --git a/.changeset/isolate-linux-network.md b/.changeset/isolate-linux-network.md new file mode 100644 index 00000000000..13122540b8d --- /dev/null +++ b/.changeset/isolate-linux-network.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"kilo-code": minor +--- + +Enforce the sandbox network restriction for agent commands on Linux, including TCP, UDP, IPv4, IPv6, and descendant processes. diff --git a/.changeset/jetbrains-clickable-prompt-mentions.md b/.changeset/jetbrains-clickable-prompt-mentions.md new file mode 100644 index 00000000000..afbcb1d4231 --- /dev/null +++ b/.changeset/jetbrains-clickable-prompt-mentions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Highlight rendered prompt file mentions and open them on click. diff --git a/.changeset/jetbrains-empty-mention-completion.md b/.changeset/jetbrains-empty-mention-completion.md new file mode 100644 index 00000000000..3fb5501937c --- /dev/null +++ b/.changeset/jetbrains-empty-mention-completion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show JetBrains file mention suggestions immediately for empty `@` mentions and keep the completion popup stable while typing quickly. diff --git a/.changeset/jetbrains-file-mention-parity.md b/.changeset/jetbrains-file-mention-parity.md new file mode 100644 index 00000000000..0f989b16040 --- /dev/null +++ b/.changeset/jetbrains-file-mention-parity.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Hide raw file contents from mentioned files in JetBrains chat messages. diff --git a/.changeset/jetbrains-git-changes-data-url.md b/.changeset/jetbrains-git-changes-data-url.md new file mode 100644 index 00000000000..5085f5ee663 --- /dev/null +++ b/.changeset/jetbrains-git-changes-data-url.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Fix @git-changes mentions causing JetBrains chat sessions to fail. diff --git a/.changeset/jetbrains-hide-read-payloads.md b/.changeset/jetbrains-hide-read-payloads.md deleted file mode 100644 index 6216844f0cd..00000000000 --- a/.changeset/jetbrains-hide-read-payloads.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Hide generated read-tool payload lines from JetBrains prompt bubbles while keeping attachments and assistant tool output visible. diff --git a/.changeset/jetbrains-horizontal-attachments.md b/.changeset/jetbrains-horizontal-attachments.md deleted file mode 100644 index 2b167a0198c..00000000000 --- a/.changeset/jetbrains-horizontal-attachments.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show JetBrains prompt attachments in one horizontal scrolling row in session history. diff --git a/.changeset/jetbrains-hover-copy-overlay.md b/.changeset/jetbrains-hover-copy-overlay.md deleted file mode 100644 index 6f04d4137bd..00000000000 --- a/.changeset/jetbrains-hover-copy-overlay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show a hover copy button for JetBrains session code and tool output blocks. diff --git a/.changeset/jetbrains-mention-icons-priority.md b/.changeset/jetbrains-mention-icons-priority.md new file mode 100644 index 00000000000..bf968c1855a --- /dev/null +++ b/.changeset/jetbrains-mention-icons-priority.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show file type icons and keep predefined mentions first in JetBrains mention completions. diff --git a/.changeset/jetbrains-message-toolbar.md b/.changeset/jetbrains-message-toolbar.md deleted file mode 100644 index 2bffa7d3be3..00000000000 --- a/.changeset/jetbrains-message-toolbar.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Add copy buttons below JetBrains session prompts and assistant responses. diff --git a/.changeset/jetbrains-model-provider-label.md b/.changeset/jetbrains-model-provider-label.md deleted file mode 100644 index 94351c71a22..00000000000 --- a/.changeset/jetbrains-model-provider-label.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show provider names on JetBrains model picker buttons for non-Kilo Gateway models. diff --git a/.changeset/jetbrains-paste-attachments.md b/.changeset/jetbrains-paste-attachments.md deleted file mode 100644 index 1003291b27a..00000000000 --- a/.changeset/jetbrains-paste-attachments.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support pasting files and images into JetBrains chat prompts as attachments. diff --git a/.changeset/jetbrains-prompt-attachment-preview.md b/.changeset/jetbrains-prompt-attachment-preview.md deleted file mode 100644 index f5b33305c46..00000000000 --- a/.changeset/jetbrains-prompt-attachment-preview.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show JetBrains prompt attachments inside the prompt bubble with previews and open embedded attachments in editor tabs. diff --git a/.changeset/jetbrains-prompt-attachments.md b/.changeset/jetbrains-prompt-attachments.md deleted file mode 100644 index efde8186459..00000000000 --- a/.changeset/jetbrains-prompt-attachments.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support sending file and image attachments from the JetBrains chat prompt. diff --git a/.changeset/jetbrains-prompt-completion.md b/.changeset/jetbrains-prompt-completion.md new file mode 100644 index 00000000000..552dfa7ccc2 --- /dev/null +++ b/.changeset/jetbrains-prompt-completion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Add `/` slash commands and `@` file/git-changes mentions to the JetBrains chat prompt with native completion. diff --git a/.changeset/jetbrains-prompt-hint.md b/.changeset/jetbrains-prompt-hint.md new file mode 100644 index 00000000000..a69a0e95f75 --- /dev/null +++ b/.changeset/jetbrains-prompt-hint.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show a concise single-line hint in the JetBrains prompt placeholder. diff --git a/.changeset/jetbrains-prompt-input-height.md b/.changeset/jetbrains-prompt-input-height.md deleted file mode 100644 index 120f0ff4dd6..00000000000 --- a/.changeset/jetbrains-prompt-input-height.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Limit JetBrains prompt input growth to the session while preserving scrolling for long prompts. diff --git a/.changeset/jetbrains-prompt-mentions-undo.md b/.changeset/jetbrains-prompt-mentions-undo.md new file mode 100644 index 00000000000..ad71522d818 --- /dev/null +++ b/.changeset/jetbrains-prompt-mentions-undo.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Fix prompt undo/redo, clean mid-token mention completion, and show unresolved file mentions in the JetBrains chat prompt. diff --git a/.changeset/jetbrains-provider-disconnect.md b/.changeset/jetbrains-provider-disconnect.md deleted file mode 100644 index e76aeccb88b..00000000000 --- a/.changeset/jetbrains-provider-disconnect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show Connect for available JetBrains providers without explicit auth metadata, keep only actually configured providers disconnectable, and reduce provider settings diagnostics to debug logs. diff --git a/.changeset/jetbrains-provider-headless-oauth.md b/.changeset/jetbrains-provider-headless-oauth.md deleted file mode 100644 index dc87eced073..00000000000 --- a/.changeset/jetbrains-provider-headless-oauth.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Prefer remote-safe provider OAuth methods in JetBrains and show device-code authorization details when available. diff --git a/.changeset/jetbrains-provider-oauth-cancel.md b/.changeset/jetbrains-provider-oauth-cancel.md deleted file mode 100644 index 41e496c2aef..00000000000 --- a/.changeset/jetbrains-provider-oauth-cancel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show cancellable OAuth progress in JetBrains provider settings and prevent starting another provider action while one is running. diff --git a/.changeset/jetbrains-restartless-unload.md b/.changeset/jetbrains-restartless-unload.md deleted file mode 100644 index 62e2f7941a7..00000000000 --- a/.changeset/jetbrains-restartless-unload.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Stop Kilo backend processes and clear JetBrains UI resources during restartless plugin unloads. diff --git a/.changeset/jetbrains-send-typed-mentions.md b/.changeset/jetbrains-send-typed-mentions.md new file mode 100644 index 00000000000..3921131f9b8 --- /dev/null +++ b/.changeset/jetbrains-send-typed-mentions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Attach hand-typed prompt file mentions when sending immediately. diff --git a/.changeset/jetbrains-session-copy.md b/.changeset/jetbrains-session-copy.md deleted file mode 100644 index a1b74c1739d..00000000000 --- a/.changeset/jetbrains-session-copy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix copying selected text from JetBrains session views. diff --git a/.changeset/jetbrains-session-drop-overlay.md b/.changeset/jetbrains-session-drop-overlay.md deleted file mode 100644 index 64be585e380..00000000000 --- a/.changeset/jetbrains-session-drop-overlay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support dropping files anywhere in a JetBrains chat session to add them to the prompt. diff --git a/.changeset/jetbrains-session-focus-restore.md b/.changeset/jetbrains-session-focus-restore.md new file mode 100644 index 00000000000..dee4790f7f6 --- /dev/null +++ b/.changeset/jetbrains-session-focus-restore.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Restore prompt focus after returning from session history in JetBrains. diff --git a/.changeset/jetbrains-session-icons.md b/.changeset/jetbrains-session-icons.md deleted file mode 100644 index 7787da11995..00000000000 --- a/.changeset/jetbrains-session-icons.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Polish session header controls and align session view icons. diff --git a/.changeset/jetbrains-slash-aliases.md b/.changeset/jetbrains-slash-aliases.md new file mode 100644 index 00000000000..bfc7284a57d --- /dev/null +++ b/.changeset/jetbrains-slash-aliases.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support VS Code slash-command aliases in the JetBrains prompt. diff --git a/.changeset/jetbrains-stable-mention-completion.md b/.changeset/jetbrains-stable-mention-completion.md new file mode 100644 index 00000000000..c4578ede044 --- /dev/null +++ b/.changeset/jetbrains-stable-mention-completion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Keep JetBrains prompt mention suggestions stable while typing fuzzy file matches. diff --git a/.changeset/jetbrains-vfs-frontend-handoff.md b/.changeset/jetbrains-vfs-frontend-handoff.md deleted file mode 100644 index 51914683663..00000000000 --- a/.changeset/jetbrains-vfs-frontend-handoff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Open embedded JetBrains message attachments in frontend-managed Kilo editor tabs with loading and connection retry states. diff --git a/.changeset/keep-sandbox-toggle-drafts.md b/.changeset/keep-sandbox-toggle-drafts.md new file mode 100644 index 00000000000..00b8347bd1b --- /dev/null +++ b/.changeset/keep-sandbox-toggle-drafts.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Preserve new-chat prompts and attachments when toggling sandbox before sending the first message. diff --git a/.changeset/kilo-branding-leftovers.md b/.changeset/kilo-branding-leftovers.md deleted file mode 100644 index b9db0630931..00000000000 --- a/.changeset/kilo-branding-leftovers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Replace remaining OpenCode-branded CLI and TUI copy with Kilo branding. diff --git a/.changeset/list-agent-manager-worktrees.md b/.changeset/list-agent-manager-worktrees.md deleted file mode 100644 index 2e15a890ad3..00000000000 --- a/.changeset/list-agent-manager-worktrees.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Show Agent Manager and other Git worktrees in the Kilo Console project view. diff --git a/.changeset/load-bundled-atomic-chat.md b/.changeset/load-bundled-atomic-chat.md deleted file mode 100644 index 0931695aca8..00000000000 --- a/.changeset/load-bundled-atomic-chat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Load the bundled Atomic Chat integration without attempting to install an unpublished npm plugin. diff --git a/.changeset/macos-network-sandbox.md b/.changeset/macos-network-sandbox.md new file mode 100644 index 00000000000..d36fc0c2c7f --- /dev/null +++ b/.changeset/macos-network-sandbox.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"kilo-code": minor +--- + +Block outbound network access from agent commands and in-process HTTP tools with the optional macOS sandbox, with a Sandboxing setting to allow network access when needed. diff --git a/.changeset/minimax-m3-reasoning-toggle.md b/.changeset/minimax-m3-reasoning-toggle.md deleted file mode 100644 index 90839cc0403..00000000000 --- a/.changeset/minimax-m3-reasoning-toggle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/cli": patch ---- - -Add an instant/thinking reasoning toggle for MiniMax M-series models, matching the existing glm/kimi/qwen behavior. diff --git a/.changeset/mix-marketplace-categories.md b/.changeset/mix-marketplace-categories.md new file mode 100644 index 00000000000..f922d06b6b7 --- /dev/null +++ b/.changeset/mix-marketplace-categories.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Browse skills, agents, and MCP servers together and filter them by category. diff --git a/.changeset/move-worktrees-safely.md b/.changeset/move-worktrees-safely.md deleted file mode 100644 index e77ccea53c2..00000000000 --- a/.changeset/move-worktrees-safely.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Clean up incomplete worktrees when moving a session fails to transfer its Git changes. diff --git a/.changeset/name-compact-settings-tabs.md b/.changeset/name-compact-settings-tabs.md deleted file mode 100644 index e462423736b..00000000000 --- a/.changeset/name-compact-settings-tabs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Announce every Settings tab by name to screen readers in compact sidebars. diff --git a/.changeset/native-plan-architect-parity.md b/.changeset/native-plan-architect-parity.md deleted file mode 100644 index 2de5bf1895f..00000000000 --- a/.changeset/native-plan-architect-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Make native Plan mode follow Architect-style planning behavior while preserving Plan mode restrictions and repo-root plan files. diff --git a/.changeset/neat-jetbrains-provider-list.md b/.changeset/neat-jetbrains-provider-list.md deleted file mode 100644 index 780e1816321..00000000000 --- a/.changeset/neat-jetbrains-provider-list.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Organize JetBrains provider settings into connected, popular, and all-provider sections, hide custom-provider creation, and prevent Kilo Gateway disconnects from provider settings. diff --git a/.changeset/notify-background-sessions.md b/.changeset/notify-background-sessions.md deleted file mode 100644 index 6b5646bcf73..00000000000 --- a/.changeset/notify-background-sessions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Offer opt-in attention sounds when sessions finish, error, or need input across the sidebar, editor tabs, and Agent Manager. diff --git a/.changeset/notify-sandbox-state.md b/.changeset/notify-sandbox-state.md new file mode 100644 index 00000000000..7088847fc59 --- /dev/null +++ b/.changeset/notify-sandbox-state.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show sandbox state changes as concise VS Code notifications. diff --git a/.changeset/open-worktree-pull-request.md b/.changeset/open-worktree-pull-request.md deleted file mode 100644 index 90a58a34c89..00000000000 --- a/.changeset/open-worktree-pull-request.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Open the selected Agent Manager worktree's pull request with Cmd/Ctrl+Shift+R. diff --git a/.changeset/opencode-v1-14-51-to-v1-15-4.md b/.changeset/opencode-v1-14-51-to-v1-15-4.md deleted file mode 100644 index 84907dc90cd..00000000000 --- a/.changeset/opencode-v1-14-51-to-v1-15-4.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Changes from opencode v1.14.51 to v1.15.4 upstream: - -- Core Improvements: Clarified how to recover when the npm package is installed without its native binary. -- Core Improvements: Reduced unnecessary prompting around shell, task, and todo flows. -- Core Bugfixes: Ignored invalid exports in custom tool modules instead of failing tool loading. -- Core Bugfixes: Ignored project instruction lookup errors so sessions keep loading when project instruction discovery fails. -- Core Bugfixes: Fixed versioned event projector lookups so event replay uses the right handlers. -- Core Bugfixes: Avoid duplicate consecutive entries in prompt history. -- Core Bugfixes: Show full config validation errors during TUI startup instead of a generic failure. -- Core Bugfixes: Fixed npm installs so the CLI can recover and fetch the right native binary on more setups. -- Core Bugfixes: Fixed multiline `@` mentions in prompts. -- Core Bugfixes: Preserved custom tool metadata from Zod schemas. -- Core Bugfixes: Preserved custom tool argument descriptions in generated schemas. -- Core Bugfixes: Fixed file watching in repos where `.git` is a symlink. (@kagura-agent) -- Core Bugfixes: Fixed sync events not reaching project-scoped subscribers in injected instances. -- Core Bugfixes: Reduced wasted work when reading very large files after output truncation. -- Core Bugfixes: Fixed project-scoped bus events so file watcher and update notifications reach the right instance. -- Core Bugfixes: Fixed custom LSP servers not sending refresh events after they initialize. -- Core Bugfixes: Hid background subagent task instructions unless experimental background mode is enabled. -- TUI Improvements: Added a collapsed thinking view that can be expanded inline. -- TUI Improvements: Added pinned sessions with quick-switch slots in the session picker. -- TUI Improvements: Newly pinned sessions now stay at the end of the pinned list instead of jumping to the top. -- TUI Improvements: Made Markdown H1 headings easier to distinguish. -- TUI Bugfixes: Fixed thinking mode defaults so reasoning starts collapsed consistently. -- TUI Bugfixes: Limited session quick-switching to pinned sessions. -- TUI Bugfixes: Fixed Markdown table rendering in chat output. -- TUI Bugfixes: Fixed `kilo run --agent` resolving project-local agents. -- TUI Bugfixes: Fixed async commands losing the active instance context, which could break agent generation and GitHub-driven runs. diff --git a/.changeset/persist-index-updates.md b/.changeset/persist-index-updates.md new file mode 100644 index 00000000000..a35078a3c5c --- /dev/null +++ b/.changeset/persist-index-updates.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Preserve unchanged codebase indexes when extension or VS Code updates interrupt an incremental scan. diff --git a/.changeset/plan-timestamp-prefix.md b/.changeset/plan-timestamp-prefix.md deleted file mode 100644 index d757c9686bf..00000000000 --- a/.changeset/plan-timestamp-prefix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Restore session timestamp prefixes for generated plan filenames while preserving descriptive model-chosen names. diff --git a/.changeset/polish-vscode-tool-previews.md b/.changeset/polish-vscode-tool-previews.md deleted file mode 100644 index 62f96ed3ed3..00000000000 --- a/.changeset/polish-vscode-tool-previews.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Polish chat tool-call previews for shell, search, and background-process output. diff --git a/.changeset/preserve-cli-telemetry-exit.md b/.changeset/preserve-cli-telemetry-exit.md deleted file mode 100644 index 2eaad21f271..00000000000 --- a/.changeset/preserve-cli-telemetry-exit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent unreachable telemetry endpoints from blocking or failing completed CLI commands. diff --git a/.changeset/preserve-console-review-expansion.md b/.changeset/preserve-console-review-expansion.md deleted file mode 100644 index 3b7f19283af..00000000000 --- a/.changeset/preserve-console-review-expansion.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Keep expanded Kilo Console file diffs open while resizing the context sidebar. diff --git a/.changeset/preserve-console-terminals.md b/.changeset/preserve-console-terminals.md deleted file mode 100644 index 476e44608f8..00000000000 --- a/.changeset/preserve-console-terminals.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Keep Kilo Console terminal sessions open when changing diff layout and other console preferences. diff --git a/.changeset/prevent-atomic-chat-install.md b/.changeset/prevent-atomic-chat-install.md deleted file mode 100644 index 03367e39a72..00000000000 --- a/.changeset/prevent-atomic-chat-install.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent the bundled Atomic Chat plugin from triggering an npm installation. diff --git a/.changeset/quiet-auto-approved-permissions.md b/.changeset/quiet-auto-approved-permissions.md new file mode 100644 index 00000000000..c56014978c4 --- /dev/null +++ b/.changeset/quiet-auto-approved-permissions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep attention sounds silent for permission requests handled by auto-approve. diff --git a/.changeset/quiet-interrupted-sessions.md b/.changeset/quiet-interrupted-sessions.md deleted file mode 100644 index c9aba17f30a..00000000000 --- a/.changeset/quiet-interrupted-sessions.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Skip attention sounds when a session is manually interrupted. diff --git a/.changeset/readable-agent-chat.md b/.changeset/readable-agent-chat.md deleted file mode 100644 index 1a5ff514a11..00000000000 --- a/.changeset/readable-agent-chat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Keep conversations readable across the sidebar, editor tabs, and Agent Manager by aligning messages, tool output, and the composer in one centered lane. diff --git a/.changeset/remote-agent-manager-sessions.md b/.changeset/remote-agent-manager-sessions.md deleted file mode 100644 index 438306342eb..00000000000 --- a/.changeset/remote-agent-manager-sessions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Enable remote control from Agent Manager without disrupting concurrent sidebar sessions, and include all open Agent Manager sessions. diff --git a/.changeset/remove-chat-input-divider.md b/.changeset/remove-chat-input-divider.md deleted file mode 100644 index efb30d27edb..00000000000 --- a/.changeset/remove-chat-input-divider.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Remove the divider between agent conversations and the prompt input across extension chat views. diff --git a/.changeset/render-image-diffs.md b/.changeset/render-image-diffs.md deleted file mode 100644 index 7445a209c7d..00000000000 --- a/.changeset/render-image-diffs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Render image changes in diff viewers and open images with VS Code's image preview. diff --git a/.changeset/render-jetbrains-attachment-cards.md b/.changeset/render-jetbrains-attachment-cards.md deleted file mode 100644 index fc26c664e3c..00000000000 --- a/.changeset/render-jetbrains-attachment-cards.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Render file attachments as attachment cards in JetBrains prompts and session history. diff --git a/.changeset/render-jetbrains-shell-markdown.md b/.changeset/render-jetbrains-shell-markdown.md deleted file mode 100644 index 267e7bd8059..00000000000 --- a/.changeset/render-jetbrains-shell-markdown.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Render JetBrains shell tool output with markdown code blocks. diff --git a/.changeset/restore-authenticated-speech-input.md b/.changeset/restore-authenticated-speech-input.md deleted file mode 100644 index 6f826ad0660..00000000000 --- a/.changeset/restore-authenticated-speech-input.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@kilocode/cli": patch -"@kilocode/sdk": patch -"kilo-code": patch ---- - -Restore speech input when profile details are unavailable, move transcription model selection to the Models tab, and default transcription to Whisper Large V3 Turbo. diff --git a/.changeset/restore-jetbrains-popular-providers.md b/.changeset/restore-jetbrains-popular-providers.md deleted file mode 100644 index 3157464e9ec..00000000000 --- a/.changeset/restore-jetbrains-popular-providers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Restore popular provider suggestions in JetBrains provider settings when provider metadata is unavailable. diff --git a/.changeset/restore-popular-providers.md b/.changeset/restore-popular-providers.md deleted file mode 100644 index 853465571b7..00000000000 --- a/.changeset/restore-popular-providers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Restore popular provider suggestions in the VS Code provider settings panel when provider metadata is unavailable. diff --git a/.changeset/sandbox-agent-writes.md b/.changeset/sandbox-agent-writes.md new file mode 100644 index 00000000000..638c8ac4e52 --- /dev/null +++ b/.changeset/sandbox-agent-writes.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": minor +"kilo-code": minor +--- + +Confine agent shell and file-tool writes to project and Kilo state directories with the optional macOS and Linux sandboxes. diff --git a/.changeset/search-agent-manager-sidebar.md b/.changeset/search-agent-manager-sidebar.md deleted file mode 100644 index e47bae5fc41..00000000000 --- a/.changeset/search-agent-manager-sidebar.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Search and switch between local sessions, worktrees, and their sessions from the Agent Manager sidebar. diff --git a/.changeset/secure-sandbox-mutations.md b/.changeset/secure-sandbox-mutations.md new file mode 100644 index 00000000000..63eb8fdb087 --- /dev/null +++ b/.changeset/secure-sandbox-mutations.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent sandboxed file tools from escaping project write roots through concurrent symlink replacement on macOS. diff --git a/.changeset/secure-worktree-sandboxes.md b/.changeset/secure-worktree-sandboxes.md new file mode 100644 index 00000000000..72418e02591 --- /dev/null +++ b/.changeset/secure-worktree-sandboxes.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Confine sandboxed worktree sessions to their active worktree instead of allowing writes to sibling or primary checkouts. diff --git a/.changeset/share-worktree-indexes.md b/.changeset/share-worktree-indexes.md deleted file mode 100644 index fcb6db6bf68..00000000000 --- a/.changeset/share-worktree-indexes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": minor -"@kilocode/kilo-indexing": minor ---- - -Share the main codebase index with Agent Manager worktrees while indexing and searching only each worktree's changed files. diff --git a/.changeset/shared-provider-icons.md b/.changeset/shared-provider-icons.md deleted file mode 100644 index b4d00ef1ed6..00000000000 --- a/.changeset/shared-provider-icons.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch -"@kilocode/cli": patch -"kilo-code": patch ---- - -Show shared provider descriptions and provider icons in JetBrains and VS Code provider settings. diff --git a/.changeset/show-console-training-filter.md b/.changeset/show-console-training-filter.md deleted file mode 100644 index 7bb387ee3dd..00000000000 --- a/.changeset/show-console-training-filter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Expose the prompt-training model filter in the Kilo Console model settings. diff --git a/.changeset/show-edit-approval-line-numbers.md b/.changeset/show-edit-approval-line-numbers.md deleted file mode 100644 index 4dbb443f3d2..00000000000 --- a/.changeset/show-edit-approval-line-numbers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Show line numbers in edit approval diffs, including compact sidebar views. diff --git a/.changeset/show-routed-step-models.md b/.changeset/show-routed-step-models.md new file mode 100644 index 00000000000..04d06d28a4f --- /dev/null +++ b/.changeset/show-routed-step-models.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Show the concrete model reported for routed Kilo auto-model steps in CLI and VS Code session timelines. diff --git a/.changeset/silent-comet-attachments.md b/.changeset/silent-comet-attachments.md deleted file mode 100644 index fb622dba660..00000000000 --- a/.changeset/silent-comet-attachments.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Open embedded transcript attachments in stable Kilo editor tabs. diff --git a/.changeset/steady-codex-refresh.md b/.changeset/steady-codex-refresh.md deleted file mode 100644 index 2d8438541e1..00000000000 --- a/.changeset/steady-codex-refresh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent concurrent Kilo processes from reusing a ChatGPT Codex refresh token. diff --git a/.changeset/switch-model-agent-links.md b/.changeset/switch-model-agent-links.md deleted file mode 100644 index 0aad2e12d12..00000000000 --- a/.changeset/switch-model-agent-links.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Support VS Code switch links that select a Kilo model, agent, or both. diff --git a/.changeset/terminal-title-status.md b/.changeset/terminal-title-status.md deleted file mode 100644 index 4234657571b..00000000000 --- a/.changeset/terminal-title-status.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": minor -"@kilocode/sdk": minor ---- - -Add opt-in Unicode or emoji terminal title indicators for sessions that are working, need attention, or have finished. diff --git a/.changeset/tidy-indexing-button.md b/.changeset/tidy-indexing-button.md new file mode 100644 index 00000000000..9b5fe509aaa --- /dev/null +++ b/.changeset/tidy-indexing-button.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Let users hide the codebase indexing button while indexing is off. diff --git a/.changeset/toggle-tui-sandbox.md b/.changeset/toggle-tui-sandbox.md new file mode 100644 index 00000000000..a616acbc636 --- /dev/null +++ b/.changeset/toggle-tui-sandbox.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": minor +"@kilocode/sdk": minor +"kilo-code": patch +--- + +Add session-local macOS sandbox controls, show the effective active state, and confirm toggles in the CLI and VS Code extension. diff --git a/.changeset/vscode-cli-version-marker-packaging.md b/.changeset/vscode-cli-version-marker-packaging.md new file mode 100644 index 00000000000..0e418c11a27 --- /dev/null +++ b/.changeset/vscode-cli-version-marker-packaging.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Stop shipping the local-only `.cli-version` build marker in packaged VSIX installs, which previously made production installs detect as local builds and inject a dev-only bwrap fallback. diff --git a/.changeset/widen-chat-readable-lane.md b/.changeset/widen-chat-readable-lane.md deleted file mode 100644 index a3d40fe1412..00000000000 --- a/.changeset/widen-chat-readable-lane.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Widen the sidebar chat readable lane from 78ch to 88ch so tools and content use a little more screen space. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..b6d8d73512f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.opencode +.sst +.turbo +.wrangler +node_modules +**/node_modules +**/.output +**/dist +**/.turbo +**/.vite +**/coverage diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index e30997d8483..f494bbb4c8e 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -41,6 +41,9 @@ jobs: - name: Check Effect Promise facade allowlist run: bun run script/check-opencode-promise-facades.ts + - name: Check model tool network boundary + run: bun run script/check-model-tool-network.ts + - name: Check workflow allowlist run: bun run script/check-workflows.ts # kilocode_change end diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e937c9a36fe..ae13d11948a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -84,6 +84,16 @@ jobs: - uses: ./.github/actions/setup-bun + # kilocode_change start + - name: Setup Zig for Linux sandbox helpers + run: | + curl --fail --location --retry 3 \ + https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" + # kilocode_change end - name: Build id: build run: | @@ -179,6 +189,14 @@ jobs: smoke_host() { binary="$1" "$binary" --version + helper="$(dirname "$binary")/bwrap" + if [[ "${{ matrix.target }}" == linux-* ]]; then + test -x "$helper" + grep -q '^SPDX-License-Identifier: LGPL-2.0-or-later$' "$(dirname "$binary")/licenses/bubblewrap/NOTICE" + "$helper" --version + "$helper" --unshare-user --disable-userns --unshare-pid --die-with-parent --new-session \ + --ro-bind / / --dev /dev --proc /proc -- "$helper" --version + fi root="$(mktemp -d)" trap 'rm -rf "$root"' RETURN ( @@ -212,8 +230,10 @@ jobs: # as optional packages and must be installed for any Bun-compiled musl binary to run. apk add --no-cache libstdc++ libgcc # kilocode_change end - binary="/dist/$PACKAGE/bin/kilo" - "$binary" --version + binary="/dist/$PACKAGE/bin/kilo" # kilocode_change + "$binary" --version # kilocode_change + "/dist/$PACKAGE/bin/bwrap" --version # kilocode_change + grep -q '^SPDX-License-Identifier: LGPL-2.0-or-later$' "/dist/$PACKAGE/bin/licenses/bubblewrap/NOTICE" # kilocode_change root="$(mktemp -d)" trap '\''rm -rf "$root"'\'' EXIT unset KILO_MODELS_PATH KILO_MODELS_URL KILO_CONFIG KILO_CONFIG_DIR @@ -288,7 +308,7 @@ jobs: $output = & $binary --pure models anthropic if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not ($output -match "(?m)^anthropic/")) { - throw "Compiled Windows binary did not list Anthropic models from the sidecar snapshot" + throw "Compiled Windows binary did not list Anthropic models from the embedded snapshot" } } finally { Remove-Item -Recurse -Force $root -ErrorAction SilentlyContinue diff --git a/.github/workflows/test-jetbrains.yml b/.github/workflows/test-jetbrains.yml index 7fde0ec1ea3..8469b98776a 100644 --- a/.github/workflows/test-jetbrains.yml +++ b/.github/workflows/test-jetbrains.yml @@ -41,6 +41,8 @@ jobs: needs: changes if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.jetbrains == 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 + container: + image: ghcr.io/kilo-org/build/jetbrains:24.04 defaults: run: shell: bash @@ -50,19 +52,11 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Bun - uses: ./.github/actions/setup-bun + - name: Mark workspace as git-safe + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-read-only: ${{ github.ref != 'refs/heads/main' }} + - name: Install dependencies + run: bun install - name: Run JetBrains unit tests run: bun script/test-ci.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c3b6b5a136..dbb18d8a4d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,8 @@ jobs: settings: - name: linux host: blacksmith-4vcpu-ubuntu-2404 # kilocode_change + - name: macos + host: macos-15 # kilocode_change - name: windows host: blacksmith-4vcpu-windows-2025 # kilocode_change runs-on: ${{ matrix.settings.host }} @@ -61,6 +63,23 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun + # kilocode_change start + - name: Setup Zig for Linux sandbox helper + if: runner.os == 'Linux' + run: | + curl --fail --location --retry 3 \ + https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" + + - name: Build Linux sandbox helper + if: runner.os == 'Linux' + run: | + bun packages/opencode/script/kilocode/bubblewrap.ts --arch x64 --output "$RUNNER_TEMP/bwrap" + echo "KILO_BWRAP_PATH=$RUNNER_TEMP/bwrap" >> "$GITHUB_ENV" + # kilocode_change end - name: Configure git identity run: | git config --global user.email "kilo-maintainer[bot]@users.noreply.github.com" @@ -79,13 +98,16 @@ jobs: run: bun turbo test:ci --filter='!@kilocode/kilo-jetbrains' env: KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} + KILO_TEST_PROFILE: ${{ runner.os == 'macOS' && github.event_name == 'pull_request' && 'darwin' || '' }} # kilocode_change + # kilocode_change start - name: Run HttpApi exerciser gates if: runner.os == 'Linux' working-directory: packages/opencode run: bun run test:httpapi + # kilocode_change end - - name: Publish unit reports + - name: Publish unit reports # kilocode_change if: always() uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0 with: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index b4d0da57a9f..11ef4e1ab75 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -21,6 +21,9 @@ jobs: # kilocode_change start - name: Run TypeScript typecheck run: bun turbo typecheck --filter='!@kilocode/kilo-jetbrains' + + - name: Build Kilo Console + run: bun turbo build --filter=@kilocode/kilo-console # kilocode_change end # kilocode_change start diff --git a/.github/workflows/watch-opencode-releases.yml b/.github/workflows/watch-opencode-releases.yml index 10da78231b2..a3d766b7aed 100644 --- a/.github/workflows/watch-opencode-releases.yml +++ b/.github/workflows/watch-opencode-releases.yml @@ -40,9 +40,12 @@ jobs: env: URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }} SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }} + VERCEL: ${{ secrets.OPENCODE_WATCH_VERCEL_SECRET }} run: | if [ -n "$URL" ] && [ -n "$SECRET" ]; then - curl -sf -X POST "$URL" -H "x-sync-secret: $SECRET" -H "Content-Type: application/json" -d '{}' || true + headers=(-H "x-sync-secret: $SECRET" -H "Content-Type: application/json") + [ -n "$VERCEL" ] && headers+=(-H "x-vercel-protection-bypass: $VERCEL") + curl -sf -X POST "$URL" "${headers[@]}" -d '{}' || true fi - if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true' diff --git a/.kilo/command/review-upstream-merge.md b/.kilo/command/review-upstream-merge.md index 36237d81c1c..ce2625a7116 100644 --- a/.kilo/command/review-upstream-merge.md +++ b/.kilo/command/review-upstream-merge.md @@ -62,6 +62,18 @@ Pay special attention to: When in doubt, add a finding. A human will verify it. Compiling code is not proof the chain is intact. +## CONFIG_REGRESSION.md + +Check whether this PR introduces or re-introduces fallback logic for `opencode` config files, or accidentally breaks code that now correctly expects only `.kilo`-based configuration. + +Kilo removed fallback support for `opencode` config directories. Look for: + +- Any new or restored code that reads from `opencode` config paths. +- Upstream additions to config discovery, loading, or path resolution that add `opencode` fallback candidates we stripped. +- Changes that break `.kilo`-only config lookup by removing or reordering it in a multi-path search. + +When in doubt, add a finding. A human should verify config path changes manually. + ## TESTS.md Check whether this PR removed any Kilo-specific tests. diff --git a/.kilo/skills/kilocode-merge-minimizer/SKILL.md b/.kilo/skills/kilocode-merge-minimizer/SKILL.md index 84dfaf4917b..9c4798de73b 100644 --- a/.kilo/skills/kilocode-merge-minimizer/SKILL.md +++ b/.kilo/skills/kilocode-merge-minimizer/SKILL.md @@ -1,12 +1,14 @@ --- name: kilocode-merge-minimizer -description: Use when changing shared upstream-owned files, editing or reviewing `kilocode_change` markers, or moving additive Kilo-specific behavior into Kilo-owned code to reduce future merge conflicts. +description: Use when changing shared upstream-owned files to add Kilo-specific behavior, editing `kilocode_change` markers in shared code, or moving additive behavior out of shared code to reduce upstream merge conflicts. Do not use for changes confined to Kilo-owned paths such as `packages/kilo-vscode/` or `packages/kilo-ui/`. --- # Kilo Merge Minimizer Use this skill whenever a normal development task touches shared upstream-owned code and includes Kilo-specific behavior, especially for marker cleanup, extraction work, or `kilocode_change` annotations. +Do not use this skill when all changes are confined to Kilo-owned paths, including `packages/kilo-vscode/`, `packages/kilo-ui/`, and paths with `kilocode` in their name. Those files are not merged from upstream and do not need merge-minimization guidance. If a task also touches shared upstream-owned code, use this skill for the shared portion only. + Do not use this as the primary guide for upstream merge resolution. Upstream merges have their own instructions and should not duplicate that workflow here. ## Goal diff --git a/.opencode-version b/.opencode-version index fff8b777303..76b19f9118b 100644 --- a/.opencode-version +++ b/.opencode-version @@ -1 +1 @@ -v1.15.4 +v1.15.13 diff --git a/.opencode/skills/improve-codebase-architecture/DEEPENING.md b/.opencode/skills/improve-codebase-architecture/DEEPENING.md deleted file mode 100644 index c52fdfd99f3..00000000000 --- a/.opencode/skills/improve-codebase-architecture/DEEPENING.md +++ /dev/null @@ -1,37 +0,0 @@ -# Deepening - -How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**. - -## Dependency categories - -When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. - -### 1. In-process - -Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed. - -### 2. Local-substitutable - -Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. - -### 3. Remote but owned (Ports & Adapters) - -Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. - -Recommendation shape: _"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."_ - -### 4. True external (Mock) - -Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. - -## Seam discipline - -- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. -- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. - -## Testing strategy: replace, don't layer - -- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them. -- Write new tests at the deepened module's interface. The **interface is the test surface**. -- Tests assert on observable outcomes through the interface, not internal state. -- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/.opencode/skills/improve-codebase-architecture/INTERFACE-DESIGN.md b/.opencode/skills/improve-codebase-architecture/INTERFACE-DESIGN.md deleted file mode 100644 index 3197723a0d0..00000000000 --- a/.opencode/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +++ /dev/null @@ -1,44 +0,0 @@ -# Interface Design - -When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best. - -Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**. - -## Process - -### 1. Frame the problem space - -Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: - -- The constraints any new interface would need to satisfy -- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md)) -- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete - -Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. - -### 2. Spawn sub-agents - -Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module. - -Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: - -- Agent 1: "Minimize the interface — aim for 1–3 entry points max. Maximise leverage per entry point." -- Agent 2: "Maximise flexibility — support many use cases and extension." -- Agent 3: "Optimise for the most common caller — make the default case trivial." -- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." - -Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. - -Each sub-agent outputs: - -1. Interface (types, methods, params — plus invariants, ordering, error modes) -2. Usage example showing how callers use it -3. What the implementation hides behind the seam -4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md)) -5. Trade-offs — where leverage is high, where it's thin - -### 3. Present and compare - -Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. - -After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu. diff --git a/.opencode/skills/improve-codebase-architecture/LANGUAGE.md b/.opencode/skills/improve-codebase-architecture/LANGUAGE.md deleted file mode 100644 index dd9b60fea07..00000000000 --- a/.opencode/skills/improve-codebase-architecture/LANGUAGE.md +++ /dev/null @@ -1,53 +0,0 @@ -# Language - -Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. - -## Terms - -**Module** -Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice. -_Avoid_: unit, component, service. - -**Interface** -Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. -_Avoid_: API, signature (too narrow — those refer only to the type-level surface). - -**Implementation** -What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. - -**Depth** -Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation. - -**Seam** _(from Michael Feathers)_ -A place where you can alter behaviour without editing in that place. The _location_ at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it. -_Avoid_: boundary (overloaded with DDD's bounded context). - -**Adapter** -A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside). - -**Leverage** -What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests. - -**Locality** -What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere. - -## Principles - -- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. -- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep. -- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape. -- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. - -## Relationships - -- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). -- **Depth** is a property of a **Module**, measured against its **Interface**. -- A **Seam** is where a **Module**'s **Interface** lives. -- An **Adapter** sits at a **Seam** and satisfies the **Interface**. -- **Depth** produces **Leverage** for callers and **Locality** for maintainers. - -## Rejected framings - -- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. -- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know. -- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. diff --git a/.opencode/skills/improve-codebase-architecture/SKILL.md b/.opencode/skills/improve-codebase-architecture/SKILL.md deleted file mode 100644 index 05984a60968..00000000000 --- a/.opencode/skills/improve-codebase-architecture/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: improve-codebase-architecture -description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. ---- - -# Improve Codebase Architecture - -Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability. - -## Glossary - -Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md). - -- **Module** — anything with an interface and an implementation (function, class, package, slice). -- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature. -- **Implementation** — the code inside. -- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation. -- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.") -- **Adapter** — a concrete thing satisfying an interface at a seam. -- **Leverage** — what callers get from depth. -- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place. - -Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list): - -- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. -- **The interface is the test surface.** -- **One adapter = hypothetical seam. Two adapters = real seam.** - -This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate. - -## Process - -### 1. Explore - -Read the project's domain glossary and any ADRs in the area you're touching first. - -Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction: - -- Where does understanding one concept require bouncing between many small modules? -- Where are modules **shallow** — interface nearly as complex as the implementation? -- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)? -- Where do tightly-coupled modules leak across their seams? -- Which parts of the codebase are untested, or hard to test through their current interface? - -Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want. - -### 2. Present candidates - -Present a numbered list of deepening opportunities. For each candidate: - -- **Files** — which files/modules are involved -- **Problem** — why the current architecture is causing friction -- **Solution** — plain English description of what would change -- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve - -**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service." - -**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. - -Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?" - -### 3. Grilling loop - -Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive. - -Side effects happen inline as decisions crystallize: - -- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist. -- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. -- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md). -- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md). diff --git a/.prettierignore b/.prettierignore index fd44c32aa5d..874473b595c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,7 +4,3 @@ packages/desktop/src/bindings.ts # alignment, which creates large spurious diffs on any unrelated content change. # See AGENTS.md "Markdown Tables" and script/check-md-table-padding.ts. *.md - -packages/opencode/src/provider/models-snapshot.ts -packages/opencode/src/provider/models-snapshot.js -packages/opencode/src/provider/models-snapshot.d.ts diff --git a/AGENTS.md b/AGENTS.md index 1a332992b49..f36d4aea2fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,14 @@ Turborepo + Bun workspaces. The packages you'll work with most: | `packages/util/` | `@opencode-ai/util` | Shared utilities (error, path, retry, slug, etc.) | | `packages/plugin/` | `@kilocode/plugin` | Plugin/tool interface definitions | +## Commits and PR Titles + +Use conventional commit-style messages and PR titles: `type(scope): summary`. + +Valid types are `feat`, `fix`, `docs`, `chore`, `refactor`, and `test`. Scopes are optional; use the affected package or area when helpful, e.g. `core`, `opencode`, `tui`, `app`, `desktop`, `sdk`, or `plugin`. + +Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributing guide`, `chore(sdk): regenerate types`. + ## Style Guide - Keep things in one function unless composable or reusable diff --git a/bun.lock b/bun.lock index 2c0e923620f..a7071637366 100644 --- a/bun.lock +++ b/bun.lock @@ -28,21 +28,21 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.3.46", + "version": "7.3.54", "bin": { "opencode": "./bin/opencode", }, "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.96", + "@ai-sdk/amazon-bedrock": "4.0.107", "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.54", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", - "@ai-sdk/google": "3.0.63", - "@ai-sdk/google-vertex": "4.0.112", + "@ai-sdk/google": "3.0.73", + "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.53", @@ -57,6 +57,7 @@ "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/sandbox": "workspace:*", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@openrouter/ai-sdk-provider": "2.9.0", @@ -67,16 +68,17 @@ "ai-gateway-provider": "3.1.2", "cross-spawn": "catalog:", "effect": "catalog:", - "gitlab-ai-provider": "6.6.0", + "gitlab-ai-provider": "6.8.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "immer": "11.1.4", + "jsonc-parser": "3.3.1", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "rotating-file-stream": "3.2.9", "semver": "^7.6.3", - "venice-ai-sdk-provider": "2.0.1", + "venice-ai-sdk-provider": "2.0.2", "xdg-basedir": "5.1.0", "zod": "catalog:", }, @@ -86,11 +88,26 @@ "@types/cross-spawn": "catalog:", "@types/npm-package-arg": "6.1.4", "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", + }, + }, + "packages/effect-drizzle-sqlite": { + "name": "@opencode-ai/effect-drizzle-sqlite", + "version": "7.3.54", + "dependencies": { + "drizzle-orm": "catalog:", + "effect": "catalog:", + }, + "devDependencies": { + "@effect/sql-sqlite-bun": "catalog:", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", }, }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:", @@ -103,11 +120,12 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", + "@pierre/diffs": "catalog:", "@solidjs/router": "catalog:", "ghostty-web": "0.4.0", "solid-js": "catalog:", @@ -124,7 +142,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -154,7 +172,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -190,7 +208,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.3.46", + "version": "7.3.54", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -200,7 +218,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -232,11 +250,24 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.3.46", + "version": "7.3.54", + }, + "packages/kilo-sandbox": { + "name": "@kilocode/sandbox", + "version": "7.3.52", + "dependencies": { + "effect": "catalog:", + }, + "devDependencies": { + "@effect/platform-node": "catalog:", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -250,7 +281,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -287,7 +318,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -339,6 +370,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", @@ -353,7 +385,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -370,7 +402,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -388,7 +420,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.3.46", + "version": "7.3.54", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -428,6 +460,7 @@ "@kilocode/kilo-telemetry": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/plugin-atomic-chat": "workspace:*", + "@kilocode/sandbox": "workspace:*", "@kilocode/sdk": "workspace:*", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", @@ -435,6 +468,7 @@ "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", + "@opencode-ai/llm": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", @@ -450,6 +484,7 @@ "@silvia-odwyer/photon-node": "0.3.4", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/scheduled": "1.5.2", + "@types/ws": "8.18.1", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", "ai-gateway-provider": "3.1.2", @@ -464,7 +499,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.6.0", + "gitlab-ai-provider": "6.8.0", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", @@ -494,6 +529,7 @@ "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", + "ws": "8.21.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "18.0.0", "zod": "catalog:", @@ -502,6 +538,7 @@ "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/core": "workspace:*", + "@opencode-ai/http-recorder": "workspace:*", "@opencode-ai/script": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", @@ -533,7 +570,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -549,9 +586,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.2.11", - "@opentui/keymap": ">=0.2.11", - "@opentui/solid": ">=0.2.11", + "@opentui/core": ">=0.2.15", + "@opentui/keymap": ">=0.2.15", + "@opentui/solid": ">=0.2.15", }, "optionalPeers": [ "@opentui/core", @@ -561,7 +598,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -575,7 +612,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "semver": "^7.6.3", }, @@ -586,7 +623,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "cross-spawn": "catalog:", }, @@ -601,7 +638,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.3.46", + "version": "7.3.54", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -624,7 +661,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.3.46", + "version": "7.3.54", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -678,7 +715,6 @@ "patchedDependencies": { "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", }, "overrides": { @@ -703,16 +739,17 @@ }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", - "@effect/opentelemetry": "4.0.0-beta.65", - "@effect/platform-node": "4.0.0-beta.65", + "@effect/opentelemetry": "4.0.0-beta.66", + "@effect/platform-node": "4.0.0-beta.66", + "@effect/sql-sqlite-bun": "4.0.0-beta.66", "@kobalte/core": "0.13.11", - "@lydell/node-pty": "1.2.0-beta.10", + "@lydell/node-pty": "1.2.0-beta.12", "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.2.11", - "@opentui/keymap": "0.2.11", - "@opentui/solid": "0.2.11", + "@opentui/core": "0.2.15", + "@opentui/keymap": "0.2.15", + "@opentui/solid": "0.2.15", "@pierre/diffs": "1.1.22", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -732,9 +769,9 @@ "cross-spawn": "7.0.6", "diff": "8.0.4", "dompurify": "3.4.2", - "drizzle-kit": "1.0.0-beta.19-d95b7a4", - "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.65", + "drizzle-kit": "1.0.0-rc.2", + "drizzle-orm": "1.0.0-rc.2", + "effect": "4.0.0-beta.66", "fuzzysort": "3.1.0", "hono": "4.12.12", "hono-openapi": "1.1.2", @@ -767,7 +804,7 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], - "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], @@ -783,15 +820,15 @@ "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqzitR171deAOWTmdqkP6okGrOvDzdDxqLnW7040OjdfsuyhtR26iL6v+zPGUtmVukwWrJnKklNbomui8y7+mw=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-awBjUhosRS9qfo5IHP1S+JMiVortq59mtlceVO4cQZ0ijbdwjv1z598umeXxggyjpgo4awFivU4uTzCtVQ+cAA=="], "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cn4+xV0menm/4JKEDElnVGiUilHvi6AD4ZK/sY7DXP/Wb7Yb3Vr86NyYM6mGBE/Shk3mWHoHbzggVnF5x0uMEA=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IkBQPEfRUbPEcWzigT76KttqoDqM4FR8H8cp5pgAw5RdDN++nUoJa0uZan3LuOp4IC8BKGZABJCqB97CMIYLdQ=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-l4t+kgOtDav2P2BJ50gZfhOYbKcGblnD0U8jXOF3WH3dczYmYfTC7JGH1/MTheurSy6UnhLw7ee4wL6StCTQ+w=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KjgM2IDMwm1qTKaW2n2qCr2ok9/wlhUC9ggNvyYYeCupEEGR8Gki1oFRA16PsBcyEr98k+kaKw4cF6D0XaVWWA=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.55", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.50", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cdlFOq4NuwZdgdwocae5ItT7uKh3mf4junJWGVKtOcs7ATY42VZLzIU2MFBgm2imGVZXwB6kcl3jd9sDBnJ8cw=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], @@ -843,61 +880,61 @@ "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1025.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ke7vyS7Dmo3St1a354AlpAjocZpG25Ql52XQ1AXRD3VQ791FBT7vX+EqGCfAEIWLPxpcldaW2Nny6P6klEOkkw=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.18", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-gy/gffKz0zaHDaqRiLCdIvgHmaAL/HXuAtMcBP7euYSFx4BsbsdlfmUBJag+Gqe62z6/XuloKyQyaiH+kS3Vrg=="], + "@aws-sdk/core": ["@aws-sdk/core@3.974.20", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@aws-sdk/xml-builder": "^3.972.29", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g=="], - "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.24", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-i6eMWlKfgQkNY3S/kg1ZnBZm2lhd6r8B3yobCalvrfCCGjvthREwsyDViuWl7gOWSvuUjQEFFGcJhGbCstcqJg=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.45", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-4L/REssieLON1hVsTzZucP1p2u5jmPNejyh/9BCGZXr93IalBbhRPCrtKIKwMTu9yRGr/bcKzhrQocByKLSzLQ=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xfUt2CUZDC+Tf16A6roD1b4pk/nrXdkoLY3TEhv198AXDtBo5xUJP1zd0e8SmuKLN4PpIBX96OizZbmMlcI6oQ=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-hjNeYb6oLyHgMihra83ie0J/T2y9om3cy1qC90h9DRgvYXEoN4BCFf8bHguZjKhXunnv7YkmZRuYL5Mkk77eCA=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.48", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg=="], "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-login": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PuQ7e8WYzAPpzvFcajxf8c0LqSzakVHVlKw8M0oubk8Kf347YOCCqT1seQrHs5AdZuIh2RD9LX4O+Xa5ImEBfQ=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bBmWDmtSpmLOZR6a0kmowBcVL1hiL8Vlap/RXeMpFd7JbWl87YcwqL6T9LH/0oBVEZXu1dUZAtojgSuZgMO5xw=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.32", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-ini": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-9aj0x9hGYUondBZSD0XkksAdHhOKttFw4BWpLCeggeg40qSJxGrAP++g0GCm0VqWc1WtC/NRFiAVzPCy56vmog=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.55", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-ini": "^3.972.53", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.52", "@aws-sdk/credential-provider-web-identity": "^3.972.52", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1CZvfb1WzudWWIFAVQkd1OI/T1RxPcSvNWzNsb2BMBVsBJzBtB8dV5f2nymHVU4UqwxipdVt/DAbgdDRf33JDg=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/token-providers": "3.1032.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-x8Mx18S48XMl9bEEpYwmXDTvjWGPIfDadReN37Lc099/DUrlL4Zs9T9rwwggo6DkKS1aev6v+MTUx7JTa87TZQ=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/token-providers": "3.1066.0", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zfuNMIkGfjYsHis9qytYf74Bcmq6Ji9Xwf4w53baRCI/b2otTwZv3SW1uRiJ5Di7999QzRGhHZ96+eUeo3gSOA=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA=="], "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1025.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1025.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-cognito-identity": "^3.972.21", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-hOMHzYetTwnpvfbLN8emaw+nnQrqlEV0I5rTrgRKTAx1anzEvls/rD1IXwOvX8Z+B9mgbK+yNFqO3wQkBghI1g=="], - "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg=="], + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.21", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-mVC0hOmwGJmNFezZ+wM8Sqfap/LjsMavEf2Evl0YWrLAcrdZOEdjnY8nRvgakVViWJSGm2eJxLuPVHGdeV06kA=="], - "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ=="], + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.17", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tdbnXbw73ww62ABWP0G0Z/euvFowEEvAoi/zG4NaZo7HJFpfGho/Z65HyVzkJLT1cMsUregr4pTyxljlarT0wA=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-+SRZL54/T9Ryxa/NmHM7WZAliU6wnfzeosT/+4IVuTgq0zSCpPx2j3yaEP4JFZlvWvoOCbKgr+4tBqSAG/dl5Q=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-8oOvo7XNDeOlwa5ys2Q0UTLCKmtvRiqf8rOU63lKniPmP3dnI5lnoy9dteZ79lxb9hmXCrO28aZMqds6C5AEoA=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-q4H/CoOYrTbyAW0d9RrHf9kTYKVXpAwoK0VEy3UT2Asad+6aa6vzQgz35dh20tRA7zlEo/Nsyjy9PVlHgdq0Vg=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-L+hXN2HDomlIsWSHW5DVD7ppccCeRnlHXZ5uHG34ePTjF5bm0I1fmrJLbUGiW97xRXWryit5cjdP4Sx2FwiGog=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-5JIDcDFWy3DUW95sOlXLbeb7RkGFUcNh1QidKsznqtlm5YGsXP0EGWaqzxBTvVmOhqKs2RmNmI6w9V/5dS3CLQ=="], - "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.16", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/eventstream-codec": "^4.2.14", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg=="], + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SCW06Zjugn86pq7+dxGnFcyWJuEWHT753HTU/Vj/OzVxP+NoShwdAr4ynxAcvWL883OgRVbSqW3ohnjIxwXjjw=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.21", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Me3d/ua2lb2G0bQfFmvCeQQp3+nN6GSPqMxDmi/IQlQ8CrlpQ5C0JJHpz2AnOUkEFI0lBNrAL3Vnt29l44ndkA=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.20", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.20", "@aws-sdk/signature-v4-multi-region": "^3.996.34", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.16", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-WY6uVMsq0EvxY4BcYhZmG2Ivd1EzNvZAqsXFlL3pTPMG0P4J83TYVQIs8P0nd5lc+Bp3llrYwggruvXzrfUtsQ=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.34", "", { "dependencies": { "@aws-sdk/types": "^3.973.12", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ=="], "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1005.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.19", "@aws-sdk/nested-clients": "^3.996.8", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.12", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-endpoints": "^3.4.1", "tslib": "^2.6.2" } }, "sha512-ty4LQxN1QC+YhUP28NfEgZDEGXkyqOQy+BDriBozqHsrYO4JMgiPhfizqOGF7P+euBTZ5Ez6SKlLAMCLo8tzmw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.19", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-W79LutZYmCV1WGe0UVGALMna3xLGP3wv2zLzrBEgc73CtzxKBxb5IpMedbS6Ej80LCknSgqFjsTtECG0IInckw=="], - "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ=="], + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.7", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA=="], - "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-Y9MPH4VZaIebwxiVK6dMzSt5L04oyNiK3NNwFe4qP5B2Hfo+pmEVpSJSa+gARPtcJeRyehUMPu5/I9DLdW0cBg=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "tslib": "^2.6.2" } }, "sha512-wsmSmHTrK9lV+5SKBekp1J7W6PufdZE08X0xv5Lz1OdgYRmyuYDy/++SslKdXhcVQjxLtzMTZaLqqLZmTx6OaQ=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.17", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-utF5qjjbuJQuU9VdCkWl7L87sr93cApsrD+uxGfUnlafX8iyEzJrb7EZnufjThURZVTOtelRMXrblWxpefElUg=="], - - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.18", "", { "dependencies": { "@smithy/types": "^4.14.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.29", "", { "dependencies": { "@smithy/types": "^4.14.3", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], @@ -907,21 +944,13 @@ "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], - "@azure-rest/core-client": ["@azure-rest/core-client@2.6.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ=="], - "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], - "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], + "@azure/core-client": ["@azure/core-client@1.10.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ=="], - "@azure/core-http-compat": ["@azure/core-http-compat@2.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA=="], - - "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], - - "@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="], - - "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.23.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ=="], + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.24.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg=="], "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], @@ -929,75 +958,71 @@ "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], - "@azure/keyvault-common": ["@azure/keyvault-common@2.1.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw=="], - - "@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="], - "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@5.7.0", "", { "dependencies": { "@azure/msal-common": "16.5.0" } }, "sha512-uYbJ0YarxkVGWEq814BysJry/IPvpDNkVKmc2bMZp4G+igUQkJ5nlFirycwPGUeA9ICLQqCxqExCA1Z1E07bPA=="], + "@azure/msal-browser": ["@azure/msal-browser@5.13.0", "", { "dependencies": { "@azure/msal-common": "16.8.0" } }, "sha512-Ea23x0U8XNFY+qJ9T44zO2BbY+AHdb+WdjmYnx36OhJ/KO+PGU5pmsNHf1DCElYX+6wyVRJz1HFeCPC/cHbRug=="], - "@azure/msal-common": ["@azure/msal-common@16.5.0", "", {}, "sha512-i3eS/5pmxDbIU/mLMENs88Qg3k6XxqJytJy6PpB7L1tCBjdXHJDadCD3Hu1TyTooe7iQo7CYqbocgL/l/8u90g=="], + "@azure/msal-common": ["@azure/msal-common@16.8.0", "", {}, "sha512-5S4RHOcInL2Nu2U217tDZbWGI6StMfcWCrA7TWvWdJmXQ+cYrrIqr84AsN62fGh2MDBysiBJPt6CfWceJfloEA=="], - "@azure/msal-node": ["@azure/msal-node@5.1.3", "", { "dependencies": { "@azure/msal-common": "16.5.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-LqT8mRZpEils9zGR9eW+Ljqifh2aMA99UF/X0jxIKDYZeHr6onlHwhVP4xHCeLhh55BI63JCbdf1iWJbMh1mPw=="], + "@azure/msal-node": ["@azure/msal-node@5.2.4", "", { "dependencies": { "@azure/msal-common": "16.8.0", "jsonwebtoken": "^9.0.0" } }, "sha512-rpBUg9dA8UpC2WiFt3KeDKVQmmmVrfxdRnW+F1ebgou/jX/0tAvYuonaq5RUo8OaqzOrj4x/HaI8DmY56RXZ2Q=="], - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], - "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -1049,23 +1074,25 @@ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], - "@docsearch/css": ["@docsearch/css@4.6.2", "", {}, "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ=="], + "@docsearch/css": ["@docsearch/css@4.6.3", "", {}, "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ=="], - "@docsearch/js": ["@docsearch/js@4.6.2", "", {}, "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw=="], + "@docsearch/js": ["@docsearch/js@4.6.3", "", {}, "sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.65", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.65" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-0CD2fSsXrDM7FP2WFkbGJO1DwMqWR3UKHh6oBDXPHAPA+RsJSKoh3pLQsbQfldLuKnhOy87Bv0v9r9IdrIHCQw=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.66", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.66" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-LU3ejAzJS+4P+Qtfn9ULnsGcIPmx1tUUB2ZswFRL+EolD8US7zMljHTwGuQRUBJOjDwt7wFCMN5AR512vdY8FQ=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.65", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.65", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.65", "ioredis": "^5.7.0" } }, "sha512-QQy3KRcMwP0TngQdfQGl2u1zp03B7k7DuF5SNS8aZhD0dDBpKZpCwFad1ODY5qdY3ycPgMwBwKRRK7y/aw0C9w=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.66", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.66", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.66", "ioredis": "^5.7.0" } }, "sha512-s/0RgaQFuszzdorRnX1PwEQNnSOi+JgMJo3zEe9O2NR3sosMhTr0Uk+1AF6bUOI9uJ2CPT3KpTIIU7q5/TpOkg=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.66", "", { "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-UYsrAb/5T0ZRypeN9Kmv3/ZInibGCjM6dtoiAWtfG+xKyuq8N05wmuVCXB0+XgVmUBxDWjw/S1fu4ivS0vZVuw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], @@ -1177,9 +1204,7 @@ "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], - - "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], @@ -1193,7 +1218,7 @@ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -1247,11 +1272,11 @@ "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - "@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], - "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="], + "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], - "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], @@ -1273,8 +1298,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="], - "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], @@ -1303,6 +1326,8 @@ "@kilocode/plugin-atomic-chat": ["@kilocode/plugin-atomic-chat@workspace:packages/plugin-atomic-chat"], + "@kilocode/sandbox": ["@kilocode/sandbox@workspace:packages/kilo-sandbox"], + "@kilocode/sdk": ["@kilocode/sdk@workspace:packages/sdk/js"], "@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="], @@ -1335,19 +1360,19 @@ "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="], + "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.12", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", "@lydell/node-pty-linux-x64": "1.2.0-beta.12", "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", "@lydell/node-pty-win32-x64": "1.2.0-beta.12" } }, "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g=="], - "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="], + "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ=="], - "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="], + "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q=="], - "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="], + "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw=="], - "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="], + "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.12", "", { "os": "linux", "cpu": "x64" }, "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ=="], - "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="], + "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g=="], - "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="], + "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.12", "", { "os": "win32", "cpu": "x64" }, "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw=="], "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], @@ -1367,37 +1392,39 @@ "@morphllm/morphsdk": ["@morphllm/morphsdk@0.2.166", "", { "dependencies": { "@vscode/ripgrep": "^1.17.0", "ai": ">=5.0.0", "diff": "^7.0.0", "isomorphic-git": "^1.25.10", "openai": "^4.52.7", "zod": ">=3.23.0" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.25.0", "@google/generative-ai": ">=0.21.0" }, "optionalPeers": ["@anthropic-ai/sdk", "@google/generative-ai"] }, "sha512-OawLa20p50rMv4EjEdHtIhi4G/AgECsOZgghPJc0159Ln6NevAjc3PynIcpS+nnk0T8yoYL6HKlLKvdzR0F0Fg=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="], + "@next/env": ["@next/env@16.2.9", "", {}, "sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.9", "", { "os": "win32", "cpu": "x64" }, "sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w=="], + + "@nodable/entities": ["@nodable/entities@2.2.0", "", {}, "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1407,7 +1434,7 @@ "@npm/types": ["@npm/types@1.0.2", "", {}, "sha512-KXZccTDEnWqNrrx6JjpJKU/wJvNeg9BDgjS0XhmlZab7br921HtyVbsYzJr4L+xIvjdJ20Wh9dgxgCI2a5CEQw=="], - "@npmcli/agent": ["@npmcli/agent@4.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA=="], + "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], "@npmcli/arborist": ["@npmcli/arborist@9.4.0", "", { "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/map-workspaces": "^5.0.0", "@npmcli/metavuln-calculator": "^9.0.2", "@npmcli/name-from-folder": "^4.0.0", "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/query": "^5.0.0", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", "minimatch": "^10.0.3", "nopt": "^9.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.0", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "pacote": "^21.0.2", "parse-conflict-json": "^5.0.1", "proc-log": "^6.0.0", "proggy": "^4.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", "semver": "^7.3.7", "ssri": "^13.0.0", "treeverse": "^3.0.0", "walk-up-path": "^4.0.0" }, "bin": { "arborist": "bin/index.js" } }, "sha512-4Bm8hNixJG/sii1PMnag0V9i/sGOX9VRzFrUiZMSBJpGlLR38f+Btl85d07G9GL56xO0l0OZjvrGNYsDYp0xKA=="], @@ -1465,15 +1492,17 @@ "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], + "@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"], + "@opencode-ai/http-recorder": ["@opencode-ai/http-recorder@workspace:packages/http-recorder"], "@opencode-ai/llm": ["@opencode-ai/llm@workspace:packages/llm"], - "@opencode-ai/plugin": ["@opencode-ai/plugin@1.4.12", "", { "dependencies": { "@opencode-ai/sdk": "1.4.12", "effect": "4.0.0-beta.48", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.1.100", "@opentui/solid": ">=0.1.100" }, "optionalPeers": ["@opentui/core", "@opentui/solid"] }, "sha512-4CVk2NFnFltrOfnXqBEb6qY9aRxh6toBx68DDrFxGDnguyH3TtYT16Reyvm83TNAAtkkdOC2f14dGPhqt+u2CA=="], + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.17.4", "", { "dependencies": { "@opencode-ai/sdk": "1.17.4", "effect": "4.0.0-beta.74", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.3.4", "@opentui/keymap": ">=0.3.4", "@opentui/solid": ">=0.3.4" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-jqXxfYnunCwyiEEsdVNDsKUhKznXytAntwzeW3uLd2zbIYsbVCiq5nG5ZSlo4mz1IXgRLWzXDYk1OnmfG15V2A=="], "@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.12", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-V8/RnAPOcvvEOKefNBuWsrd1rY23bJUYVeI2CS1CEW9bUf0Q9JNESBT7Eop6jv77VBAwzljZju54AzgsCB7ydw=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.17.4", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-OdHkBoNIQOjQsPnFtkcxAp9HsLAKn/MQiq7wIQURkRARmw8yhFqGiMPYbNm+UfLn6bMkx97rdPkWQARejMyiXQ=="], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], @@ -1483,14 +1512,12 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="], - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], @@ -1499,7 +1526,7 @@ "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="], + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], @@ -1507,65 +1534,105 @@ "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], - "@opentui/core": ["@opentui/core@0.2.11", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.11", "@opentui/core-darwin-x64": "0.2.11", "@opentui/core-linux-arm64": "0.2.11", "@opentui/core-linux-x64": "0.2.11", "@opentui/core-win32-arm64": "0.2.11", "@opentui/core-win32-x64": "0.2.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-X0zLmcDEvMrPzWYp769I7VEVb+og38vaete9tGZXu9HnJgu/paPUUplUT+6denBQccr2qx1rBYV6EtgbBpLEyw=="], + "@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h2MXtE2Cu3XlKVoQMXthnbhleO68zGXkoh/r1Q5pCoZh6RuXqns5/94D/aZThXBWwzPuEoyarMlxxR9OqrpvHw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y0jbPClnOBTPSIy+2THG86MTqIG/jGFlOOKuw4JfCDqEjPBM3pLWIHnJb3WxHRi2LlvfyBxvrUTXWlW6JpI0QQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-blQyyuTaW4q/OQ3whs7Kt7GCXhBUR5EQHHDdjOqQAr0HYpohUa6sbHMbiBcX2Ehc9ZWwtiaOoWiyZ5YXy2SAvg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-0nEB5+MgzQRYiVcQd1vHXPWNPWGh4JEmQTJKyG3OHnTzPaJ1FVSQ/V71ECyRSl3ymY3F+U0eW9cFgw1hCieK2w=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-+KKH77fzm0qF8py9G2pU32DzB1bAgDMfBajrs7gKL5NtSEnknrwfh7hIs/tq41aF6j9zvIzgtykByh26tcjFog=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-dMmb9DX0W0HWadLdgciMbonqIc1xdcKiVmaQSYxw5eGCzFRPZIOrKHByesP+2ipkMuLx85W/MJUFal/lW8XSNg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="], - "@opentui/keymap": ["@opentui/keymap@0.2.11", "", { "dependencies": { "@opentui/core": "0.2.11" }, "peerDependencies": { "@opentui/react": "0.2.11", "@opentui/solid": "0.2.11", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-pCrJrY3mTuXdDaaRneId1JsJCtGE+7prTtWihzOLZzVJTJYyYtT38gMI7MpyAoloVDfEL5cTe8C+v7wv+IYREw=="], + "@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="], - "@opentui/solid": ["@opentui/solid@0.2.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-M3WHxBFORHVE0yqMJYpi9PfjXWlnRTw/LYuBhZaJv0HTo+zTs60P/ukGcwnHDWnMpTGf3BH9x0Yi2dIqjHRY6Q=="], + "@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="], - "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="], - "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.19.1", "", { "os": "android", "cpu": "arm64" }, "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="], - "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.19.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="], - "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.19.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="], - "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.19.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="], - "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="], - "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="], - "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="], - "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="], - "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.19.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="], - "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="], - "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="], - "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.19.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="], - "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="], - "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="], - "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.19.1", "", { "os": "none", "cpu": "arm64" }, "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="], - "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.19.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="], - "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.19.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="], - "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.19.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="], - "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="], + + "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.20.0", "", { "os": "android", "cpu": "arm64" }, "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.20.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.20.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.20.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.20.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.20.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.20.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.20.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.20.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="], @@ -1655,83 +1722,81 @@ "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], - "@posthog/core": ["@posthog/core@1.25.2", "", {}, "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg=="], + "@posthog/core": ["@posthog/core@1.32.3", "", { "dependencies": { "@posthog/types": "1.386.3" } }, "sha512-vwOEMfZvGv5XxNWV7p9I52NSmvFNMhyW2IHpIoUHW5jLkgUrknzJW1H/qxVGSIrNNVQkfsoaDFzDhJdg10pgrA=="], - "@posthog/types": ["@posthog/types@1.369.3", "", {}, "sha512-Ywqvs4513PixR2TIA5O3GMEyK4F65uefwxPfsIUeHr9ruGylyXp00YJ4CEbp8U0DMzCkeF+LsMKVnHgN3pAXcA=="], + "@posthog/types": ["@posthog/types@1.386.3", "", {}, "sha512-LqJoiQi2eyWn7rCUgnn+D+F3Efp6+04o72bjSX6kWHx0nFaYNC/nJuAIRliDTY/X7GPIUAaHAcSjbMI/9wfX1Q=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], "@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.17.0", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "undici": "^6.23.0" }, "peerDependencies": { "typescript": ">=4.7" } }, "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A=="], "@qdrant/openapi-typescript-fetch": ["@qdrant/openapi-typescript-fetch@1.2.6", "", {}, "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.1", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.1", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.1", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.1", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.1", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="], "@secretlint/config-creator": ["@secretlint/config-creator@10.2.2", "", { "dependencies": { "@secretlint/types": "^10.2.2" } }, "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ=="], @@ -1767,7 +1832,7 @@ "@shikijs/themes": ["@shikijs/themes@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0" } }, "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ=="], - "@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="], + "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], "@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], @@ -1775,7 +1840,7 @@ "@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="], - "@sigstore/core": ["@sigstore/core@3.2.0", "", {}, "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA=="], + "@sigstore/core": ["@sigstore/core@3.2.1", "", {}, "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g=="], "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.5.1", "", {}, "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g=="], @@ -1783,7 +1848,7 @@ "@sigstore/tuf": ["@sigstore/tuf@4.0.2", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "tuf-js": "^4.1.0" } }, "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ=="], - "@sigstore/verify": ["@sigstore/verify@3.1.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag=="], + "@sigstore/verify": ["@sigstore/verify@3.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA=="], "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], @@ -1793,94 +1858,80 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-GFlGPNLZKrGfqWpqVb31z7hvYCA9ZscfX1buYnvvMGcRYsQQnhH+4uN6mWWflcD5jB4OXP/LBrdpukEdjl41tg=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-AXbvUX9aNY2qCLOMCikpl1Df5w2CNFEqbEb6XafG81FJbAbB8avIT7BOx1KDqiO86J/38qKQ3YuakfAfY3iBkQ=="], - "@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], + "@smithy/core": ["@smithy/core@3.24.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.14", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.8", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-BQao/dBhLCJqo953N1hadkcF3M/9G+i6qIgnMupfdpBQomwyhfV7Xfc5jjpCkm8HxfzaWAGrM/2nNnzronFqVQ=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-OUoNRXJGZMM4ivoU7QIzOvCLbavD1YnadNEairrtYhTi+gmGhyn3c2wToL9CxEs4Cw2Ab/KeQM39T1K+/e9YdQ=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M6FeKRMi3oecpTy4EL5n1hLPWydw+xInFYQIzjbGYGBnFtW7IlJjnXrKr/Ev1GpMtmw44QCmrl8+ACEFPmRsIg=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.14", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], + "@smithy/hash-node": ["@smithy/hash-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-lIZyQ7gDxURrnfkjalM0lKmDnfZYuPzNBYlkza3czPTQNVYsg4e0o90Zx/RpxhamKKOGsQGCsopp0ULsJqltNQ=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-jUH1Eth7Sgn4KPBX5OKYDRpNjzul7AzsIhxKXT1rHXPTSfY00/7Kb9RtNil5SDAlPPsxaUiesR/rql2wjackmw=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-nfpYCrzSFAgfIXmIHFTjOGNeTV3DVF5E5rfi3ZuNfsOjKSpePBOJF3rjyXlWYND0anvxVoqioIwClWCNdKt4Og=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-zdG5bJZOiM2PRgL2lwcgui6uwZ+s5y6Qsk/rk05Q69sZJT6oi1x+v8Kn++V/q9VY94EgOtEe5kivpu+eGau0wQ=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.30", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-serde": "^4.2.18", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-MWppaYUlc+W4cU2JZnYuMFeOxCWbKO4A57BWti6aCb7hRBK3+CL6llADGpX084hjImsqr3EvCGewArOj7G81eA=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.3", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/service-error-classification": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-TE8dJNi6JuxzGSxMCVd3i9IEWDndCl3bmluLsBNDWok8olgj65OfkndMhl9SZ7m14c+C5SQn/PcUmrDl57rSFw=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-I3fPVYKKEog3a3qdqt1nttP1NBuQOAlNoQxEp6j5pMogSx0HHfid63difhcDgslV6p1XsTXG6D6ieTe13ycJtQ=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.18", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-QhNiWfg47Kl4SJHmuQvnlzCtlD1eX1J7d/vuuttIE17Ra2YUKp9Srv5lCwa3OvoYaSNWMKYn0PjGIsfCLMJsEA=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-M+gG6eQ0y073mSmNB+erRXJvwpsqsN72ol2w6vcd8FEKeG7pqYK0JvzfVqONkPj2ElBB2pg+cU13I850b//Wag=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.14", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.7", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], + "@smithy/property-provider": ["@smithy/property-provider@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-0rhHv1Ww27kajF6qewme2aRtJmKFtSwE6EZ2dj5KxdX/R3ANsUugqTnH0tvpZwGiQ3MOMhetuCGFAeKVv3/Onw=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-H6S7NyaaL+7qO8kIL7VQ7KyrGnKXdllGzJqvtp3hvDen25UOydKV51qGDVK0UciW125jV3CoLJQy/ihc0OEC6A=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-In8gYD2R66EKlGAq9QrNKVrMOGaGBD7LUNp2kUjeQ4V9zNktFIXBPmrCySr4YYo+jVeVL6CnWj26sOamcF0qIg=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.13.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-tAf35/JW/DvMlACcazcoIOKOV0JBqyOvxjPTEME9W+m9wLcE0G1rwADc7Ntu38rY5C9OH8jZjpo4tbtjmIjEBQ=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1" } }, "sha512-vVimoUnGxlx4eLLQbZImdOZFOe+Zh+5ACntv8VxZuGP72LdWu5GV3oEmCahSEReBgRJoWjypFkrehSj7BWx1HQ=="], + "@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], + "@smithy/url-parser": ["@smithy/url-parser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-9MRJzwUrlswwHogOR7raDcykuzojZn74qGdQdbEQLVaixlvJuMiIT0g/CejKcmAIgrUVs8brBrnGtmYmBc0iuA=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.14", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA=="], + "@smithy/util-base64": ["@smithy/util-base64@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-V6ApAGvCQnb7Wy1Sy60AQc+7UOEaNQxvAXBLdMi5Zzm66cmX0srvfAxDmg7BGuJ+9H9ez0PPWS/AeFgWxwGavA=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.11", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg=="], + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-+3vGcNHuvzuFLVWL9/wJgucOuQWufhuGhb3oxVDj9SWFGtwkOmtC2nFUwVC2IJoPe45uhs6TAb8bgE4IXDSPzA=="], - "@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-T15zTQJ/xKYdS0/3CFckhz1QBbhxmhk/xjL6FKvHKgkJPN4E985If2FI9CcV2kh2v0sfiWMfXVEOKFbqgw4m4w=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-sms/ty2CJwHOiGzEaAVWizTVK5KusXpAYqCUeXIa+hWtNKLwjimH4z11mc07d0Fe3DT3lmZJIZWOMcVQ/N4hBQ=="], - "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-dRCZKu05AL7KQWrVuRJPotfjCRnvGkCjV56XNP067CRfyTtvgi/Ygu44qrBKb814Hsa52bWwDJ+Vt3pd04BjPA=="], - "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-tTR8tayMoa0WeRhtMH7j3WpHUtggBXjh7rBdf7j6POYI69R85gpWBW6B32kaJRnlQU8+0gOGAzJj50S7SU1Egw=="], - "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.5.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-kaB41eVUYC7ajVWUsZRqagxwRaa3VupjQ/Z2Z2v/Vffh/gJ/fFOS25s6mTyR2Lw1FrnBbRWo1iShR9BhekpPeQ=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-ooo5MQdstAtIlgS0bchoMkVsQ3x1wLLPtFilpeIV8wVtpwZYY8PoSdlvR79+yw0aJU9hjd8stKsmzIxrmAQ6fw=="], - "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.3.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-TrAgOcL63TRi7G92arTzq0n+VDrmZifwP1I1T9y2xU3lJpybsHdm33S2d3xaFfG0c8zJNIF9yYRqLSe6rbhH/A=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.47", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zlIuXai3/SHjQUQ8y3g/woLvrH573SK2wNjcDaHu5e9VOcC0JwM1MI0Sq0GZJyN3BwSUneIhpjZ18nsiz5AtQw=="], + "@smithy/util-retry": ["@smithy/util-retry@4.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-E/kFnvWQL6rIPr0Ucjk8oDgJSkKx2bv0nJkJ/cB3ywys7xCqeL1AXP9liHjgYONdQ+MKw/xT06IQK3vgbtu2Ww=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.52", "", { "dependencies": { "@smithy/config-resolver": "^4.4.16", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cQBz8g68Vnw1W2meXlkb3D/hXJU+Taiyj9P8qLJtjREEV9/Td65xi4A/H1sRQ8EIgX5qbZbvdYPKygKLholZ3w=="], - - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.4.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-wMxNDZJrgS5mQV9oxCs4TWl5767VMgOfqfZ3JHyCkMtGC2ykW9iPqMvFur695Otcc5yxLG8OKO/80tsQBxrhXg=="], - - "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], - - "@smithy/util-retry": ["@smithy/util-retry@4.3.2", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2+KTsJEwTi63NUv4uR9IQ+IFT1yu6Rf6JuoBK2WKaaJ/TRvOiOVGcXAsEqX/TQN2thR9yII21kPUJq1UV/WI2A=="], - - "@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], - - "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.6.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "tslib": "^2.6.2" } }, "sha512-g+hQ45sPnaIDU4CnaG8EufmeWwziQlcpIvPG6hVY7v65RcUgasM63J/WNfSsXEcZ1zFu9rS/r/qqfDxkIrQtDw=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], - "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], "@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="], @@ -1925,75 +1976,73 @@ "@storybook/addon-docs": ["@storybook/addon-docs@10.2.10", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.2.10", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.2.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-2wIYtdvZIzPbQ5194M5Igpy8faNbQ135nuO5ZaZ2VuttqGr+IJcGnDP42zYwbAsGs28G8ohpkbSgIzVyJWUhPQ=="], - "@storybook/addon-links": ["@storybook/addon-links@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.3.5" }, "optionalPeers": ["react"] }, "sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw=="], + "@storybook/addon-links": ["@storybook/addon-links@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react", "react"] }, "sha512-sWydPWLgduT24p/NJ/hXHcHsPlAyzQP+cOtCGliSI989K9yBP/TOL3A8sz7LIDfukI9DVAsylPhJ1jDSiAEI1w=="], - "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.3.5", "", { "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-s3/gIy9Tqxji27iclLY+KSk8kGeow1JxXMl1lPLyu8n6XVvv+tFrUPhAvUTs+fVenG6JQEWc0uzpYBdFRWbMtw=="], + "@storybook/addon-onboarding": ["@storybook/addon-onboarding@10.4.4", "", { "peerDependencies": { "storybook": "^10.4.4" } }, "sha512-ZTWGm8VXQUTepV4aEmIgHxdY7JMtn57H3uYnM3HD+qR8fmAcpLPoJ9ffXaMWUwsjK6SeferQyDTRN3q3Jd5+mg=="], "@storybook/addon-themes": ["@storybook/addon-themes@10.2.10", "", { "dependencies": { "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.2.10" } }, "sha512-j7ixCgzpWeTU7K4BkNHtEg3NdmRg9YW7ynvv0OjD3vaz4+FUVWOq7PPwb3SktLS1tOl4UA13IpApD8nSpBiY6A=="], - "@storybook/addon-vitest": ["@storybook/addon-vitest@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.3.5", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-PQDeeMwoF55kvzlhFqVKOryBJskkVk71AbDh7F0y8PdRRxlGbTvIUkKXktHZWBdESo0dV6BkeVxGQ4ZpiFxirg=="], + "@storybook/addon-vitest": ["@storybook/addon-vitest@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2" }, "peerDependencies": { "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", "storybook": "^10.4.4", "vitest": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/browser-playwright", "@vitest/runner", "vitest"] }, "sha512-VPpBwf1Elr+0g33am8ZE6aHhLB+r1TPxUsnDuCVNhxGjRxMFyQkAE8+jPJFPvS/YIUGMbVXarzaV7PcI/sJuVQ=="], - "@storybook/builder-vite": ["@storybook/builder-vite@10.3.5", "", { "dependencies": { "@storybook/csf-plugin": "10.3.5", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.3.5", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw=="], + "@storybook/builder-vite": ["@storybook/builder-vite@10.4.4", "", { "dependencies": { "@storybook/csf-plugin": "10.4.4", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.4.4", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VyuZ4mEvhhVXjJa1qXMWKH8ohnas0rgEuJDf6u4aJ54XeENFebPUEAHde1Qo2PflJ4rUdVdXieOZzKbYwP5RAQ=="], "@storybook/csf-plugin": ["@storybook/csf-plugin@10.2.10", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.2.10", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-aFvgaNDAnKMjuyhPK5ialT22pPqMN0XfPBNPeeNVPYztngkdKBa8WFqF/umDd47HxAjebq+vn6uId1xHyOHH3g=="], "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], - "@storybook/icons": ["@storybook/icons@2.0.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg=="], + "@storybook/icons": ["@storybook/icons@2.0.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw=="], "@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.2.10", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.2.10" } }, "sha512-TmBrhyLHn8B8rvDHKk5uW5BqzO1M1T+fqFNWg88NIAJOoyX4Uc90FIJjDuN1OJmWKGwB5vLmPwaKBYsTe1yS+w=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="], - "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], - "@textlint/ast-node-types": ["@textlint/ast-node-types@15.5.4", "", {}, "sha512-bVtB6VEy9U9DpW8cTt25k5T+lz86zV5w6ImePZqY1AXzSuPhqQNT77lkMPxonXzUducEIlSvUu3o7sKw3y9+Sw=="], + "@textlint/ast-node-types": ["@textlint/ast-node-types@15.7.1", "", {}, "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA=="], - "@textlint/linter-formatter": ["@textlint/linter-formatter@15.5.4", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.5.4", "@textlint/resolver": "15.5.4", "@textlint/types": "15.5.4", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-D9qJedKBLmAo+kiudop4UKgSxXMi4O8U86KrCidVXZ9RsK0NSVIw6+r2rlMUOExq79iEY81FRENyzmNVRxDBsg=="], + "@textlint/linter-formatter": ["@textlint/linter-formatter@15.7.1", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.7.1", "@textlint/resolver": "15.7.1", "@textlint/types": "15.7.1", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.18.1", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA=="], - "@textlint/module-interop": ["@textlint/module-interop@15.5.4", "", {}, "sha512-JyAUd26ll3IFF87LP0uGoa8Tzw5ZKiYvGs6v8jLlzyND1lUYCI4+2oIAslrODLkf0qwoCaJrBQWM3wsw+asVGQ=="], + "@textlint/module-interop": ["@textlint/module-interop@15.7.1", "", {}, "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g=="], - "@textlint/resolver": ["@textlint/resolver@15.5.4", "", {}, "sha512-5GUagtpQuYcmhlOzBGdmVBvDu5lKgVTjwbxtdfoidN4OIqblIxThJHHjazU+ic+/bCIIzI2JcOjHGSaRmE8Gcg=="], + "@textlint/resolver": ["@textlint/resolver@15.7.1", "", {}, "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g=="], - "@textlint/types": ["@textlint/types@15.5.4", "", { "dependencies": { "@textlint/ast-node-types": "15.5.4" } }, "sha512-mY28j2U7nrWmZbxyKnRvB8vJxJab4AxqOobLfb6iozrLelJbqxcOTvBQednadWPfAk9XWaZVMqUr9Nird3mutg=="], + "@textlint/types": ["@textlint/types@15.7.1", "", { "dependencies": { "@textlint/ast-node-types": "15.7.1" } }, "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A=="], "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], @@ -2019,7 +2068,7 @@ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -2109,7 +2158,7 @@ "@types/diff": ["@types/diff@6.0.0", "", {}, "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], @@ -2129,14 +2178,12 @@ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], "@types/mocha": ["@types/mocha@10.0.10", "", {}, "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q=="], - "@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="], - "@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -2159,12 +2206,10 @@ "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], - "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="], - "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="], @@ -2179,7 +2224,9 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/vscode": ["@types/vscode@1.116.0", "", {}, "sha512-sYHp4MO6BqJ2PD7Hjt0hlIS3tMaYsVPJrd0RUjDJ8HtOYnyJIEej0bLSccM8rE77WrC+Xox/kdBwEFDO8MsxNA=="], + "@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="], + + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], @@ -2189,25 +2236,25 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/type-utils": "8.61.0", "@typescript-eslint/utils": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.0", "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0" } }, "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.61.0", "", {}, "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.0", "@typescript-eslint/tsconfig-utils": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260316.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260316.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-s+QGNx+3zxTZBuZw3oNOFlHqpbmg0cTgBd/b6SRZ5mo3vFChkhflYqRW2IvTvU9a3PPX3bQAkQ/gWbDZCmNC3Q=="], @@ -2225,14 +2272,12 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LvpV1hyQS0U9yMLHgWexhC7oSeBpcNbIJtYC6Iyvu63Mb6J/cP0k2fQmnAVB2yesMMQFtuY6v2YIx17vE0Ymfw=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], - "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], @@ -2251,13 +2296,37 @@ "@vscode/codicons": ["@vscode/codicons@0.0.44", "", {}, "sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ=="], - "@vscode/ripgrep": ["@vscode/ripgrep@1.17.1", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-xTs7DGyAO3IsJYOCTBP8LnTvPiYVKEuyv8s0xyJDBXfs8rhBfqnZPvb6xDT+RnwWzcXqW27xLS/aGrkjX7lNWw=="], + "@vscode/ripgrep": ["@vscode/ripgrep@1.18.0", "", { "optionalDependencies": { "@vscode/ripgrep-darwin-arm64": "1.18.0", "@vscode/ripgrep-darwin-x64": "1.18.0", "@vscode/ripgrep-linux-arm": "1.18.0", "@vscode/ripgrep-linux-arm64": "1.18.0", "@vscode/ripgrep-linux-ia32": "1.18.0", "@vscode/ripgrep-linux-ppc64": "1.18.0", "@vscode/ripgrep-linux-riscv64": "1.18.0", "@vscode/ripgrep-linux-s390x": "1.18.0", "@vscode/ripgrep-linux-x64": "1.18.0", "@vscode/ripgrep-win32-arm64": "1.18.0", "@vscode/ripgrep-win32-ia32": "1.18.0", "@vscode/ripgrep-win32-x64": "1.18.0" } }, "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ=="], + + "@vscode/ripgrep-darwin-arm64": ["@vscode/ripgrep-darwin-arm64@1.18.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q=="], + + "@vscode/ripgrep-darwin-x64": ["@vscode/ripgrep-darwin-x64@1.18.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ=="], + + "@vscode/ripgrep-linux-arm": ["@vscode/ripgrep-linux-arm@1.18.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ=="], + + "@vscode/ripgrep-linux-arm64": ["@vscode/ripgrep-linux-arm64@1.18.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g=="], + + "@vscode/ripgrep-linux-ia32": ["@vscode/ripgrep-linux-ia32@1.18.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg=="], + + "@vscode/ripgrep-linux-ppc64": ["@vscode/ripgrep-linux-ppc64@1.18.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w=="], + + "@vscode/ripgrep-linux-riscv64": ["@vscode/ripgrep-linux-riscv64@1.18.0", "", { "os": "linux", "cpu": "none" }, "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw=="], + + "@vscode/ripgrep-linux-s390x": ["@vscode/ripgrep-linux-s390x@1.18.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ=="], + + "@vscode/ripgrep-linux-x64": ["@vscode/ripgrep-linux-x64@1.18.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q=="], + + "@vscode/ripgrep-win32-arm64": ["@vscode/ripgrep-win32-arm64@1.18.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q=="], + + "@vscode/ripgrep-win32-ia32": ["@vscode/ripgrep-win32-ia32@1.18.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ=="], + + "@vscode/ripgrep-win32-x64": ["@vscode/ripgrep-win32-x64@1.18.0", "", { "os": "win32", "cpu": "x64" }, "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg=="], "@vscode/test-cli": ["@vscode/test-cli@0.0.12", "", { "dependencies": { "@types/mocha": "^10.0.10", "c8": "^10.1.3", "chokidar": "^3.6.0", "enhanced-resolve": "^5.18.3", "glob": "^10.3.10", "minimatch": "^9.0.3", "mocha": "^11.7.4", "supports-color": "^10.2.2", "yargs": "^17.7.2" }, "bin": { "vscode-test": "out/bin.mjs" } }, "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ=="], "@vscode/test-electron": ["@vscode/test-electron@2.5.2", "", { "dependencies": { "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "jszip": "^3.10.1", "ora": "^8.1.0", "semver": "^7.6.2" } }, "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg=="], - "@vscode/vsce": ["@vscode/vsce@3.9.1", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA=="], + "@vscode/vsce": ["@vscode/vsce@3.9.2", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^13.0.6", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^10.2.2", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^3.2.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA=="], "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="], @@ -2307,7 +2376,7 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -2323,7 +2392,7 @@ "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -2337,6 +2406,8 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "anynum": ["anynum@1.0.0", "", {}, "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA=="], + "apache-arrow": ["apache-arrow@18.1.0", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/command-line-args": "^5.2.3", "@types/command-line-usage": "^5.0.4", "@types/node": "^20.13.0", "command-line-args": "^5.2.1", "command-line-usage": "^7.0.1", "flatbuffers": "^24.3.25", "json-bignum": "^0.0.3", "tslib": "^2.6.2" }, "bin": { "arrow2csv": "bin/arrow2csv.js" } }, "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg=="], "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], @@ -2379,13 +2450,13 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="], + "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], - "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], + "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], "azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="], - "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], @@ -2395,7 +2466,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], @@ -2403,13 +2474,13 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="], + "bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], - "bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], @@ -2425,7 +2496,7 @@ "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -2439,6 +2510,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -2465,7 +2538,7 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "cbor": ["cbor@8.1.0", "", { "dependencies": { "nofilter": "^3.1.0" } }, "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg=="], @@ -2523,7 +2596,7 @@ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], - "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], "cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="], @@ -2599,7 +2672,7 @@ "currently-unhandled": ["currently-unhandled@0.4.1", "", { "dependencies": { "array-find-index": "^1.0.1" } }, "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng=="], - "cytoscape": ["cytoscape@3.33.2", "", {}, "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw=="], + "cytoscape": ["cytoscape@3.34.0", "", {}, "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg=="], "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], @@ -2677,9 +2750,9 @@ "date-time": ["date-time@3.1.0", "", { "dependencies": { "time-zone": "^1.0.0" } }, "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg=="], - "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -2751,9 +2824,9 @@ "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], - "drizzle-kit": ["drizzle-kit@1.0.0-beta.19-d95b7a4", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-M0sqc+42TYBod6kEZ3AsW6+JWe3+76gR1aDFbHH5DmuLKEwewmbzlhBG6qnvV6YA1cIIbkuam3dC7r6PREOCXw=="], + "drizzle-kit": ["drizzle-kit@1.0.0-rc.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-TRxUmj1wDA2QCt3GvuhfamvIa66wJ7+MzSxBMKkpRtYScjHTumT9BE+x6daSzuEacSrPEuUH5/cW1uo5RkoPIg=="], - "drizzle-orm": ["drizzle-orm@1.0.0-beta.19-d95b7a4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-bZZKKeoRKrMVU6zKTscjrSH0+WNb1WEi3N0Jl4wEyQ7aQpTgHzdYY6IJQ1P0M74HuSJVeX4UpkFB/S6dtqLEJg=="], + "drizzle-orm": ["drizzle-orm@1.0.0-rc.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-UXYDkbplF5wX0hwxll+80QhEwUvAJLBu+tAK/d4fna18kLE6VuliAzufF/ieDEIJeSnLRYgtmsXD6x1Xuy1kIg=="], "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="], @@ -2767,9 +2840,9 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="], + "effect": ["effect@4.0.0-beta.66", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw=="], - "electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.372", "", {}, "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="], "emittery": ["emittery@1.2.1", "", {}, "sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw=="], @@ -2777,17 +2850,15 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="], + "engine.io-client": ["engine.io-client@6.6.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], + "enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -2803,11 +2874,11 @@ "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], + "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], @@ -2849,7 +2920,7 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.7", "", {}, "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA=="], + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], @@ -2861,7 +2932,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="], + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -2871,9 +2942,7 @@ "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - "fast-check": ["fast-check@4.7.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ=="], - - "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -2885,17 +2954,17 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-json-stringify": ["fast-json-stringify@6.3.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA=="], + "fast-json-stringify": ["fast-json-stringify@6.4.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="], + "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], - "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], @@ -2907,8 +2976,6 @@ "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -2927,7 +2994,7 @@ "find-exec": ["find-exec@1.0.3", "", { "dependencies": { "shell-quote": "^1.8.1" } }, "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug=="], - "find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="], + "find-my-way": ["find-my-way@9.6.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ=="], "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], @@ -2949,7 +3016,7 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], @@ -2963,7 +3030,7 @@ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], - "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + "framer-motion": ["framer-motion@12.40.0", "", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -2983,15 +3050,15 @@ "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + "gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="], - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gcp-metadata": ["gcp-metadata@8.1.3", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-ziTrzUhhpL9Zk5k0HHzgP/KIpWDJT0VMBC/ynt/QIBvTW+UUcSivQRl6VlwTf/EilDxtSWklHoRsKy1c4k+59w=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -3007,7 +3074,7 @@ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.8.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-KwHASXkHtDcgrzTXZVp9Dyx6t8m9nK0R2fCm47MWcxxQ1kOBt3f2LZugtu1kOby8i4Sbd+kvBSYM66PGkDclng=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -3019,13 +3086,13 @@ "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "google-logging-utils": ["google-logging-utils@1.1.4", "", {}, "sha512-LXxE6ND+JHhDPYtPFt3oeWrjxFPrnRZCZ4At6lpbi1ZDSAN7bmGET9s53vvARbxT93p+xpeDUbc/nCR4C+7vcw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], + "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="], "graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="], @@ -3035,6 +3102,8 @@ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -3043,7 +3112,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], @@ -3055,7 +3124,7 @@ "hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="], - "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], @@ -3073,7 +3142,7 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + "human-id": ["human-id@4.2.0", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], @@ -3097,6 +3166,8 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], @@ -3107,13 +3178,13 @@ "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], - "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], + "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], - "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + "ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], "irregular-plurals": ["irregular-plurals@3.5.0", "", {}, "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ=="], @@ -3121,7 +3192,7 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -3171,7 +3242,7 @@ "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - "isomorphic-git": ["isomorphic-git@1.37.5", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-wek54c5uFvd3WsxewLWt6h0GXKWQh0P8rRXns9bN1rHNjcgCb3+0lmyAsP594NeTtQFeCJQVS9b0kjbkD1l5qg=="], + "isomorphic-git": ["isomorphic-git@1.38.4", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-Ud5vs6Ac+ET+iOZWZB1j2RruVeGQSQc7U7QUhPq6iGqzifaqOVHCgRpG/8c0LwIP39R+Mr+lzR4escmCuhjONQ=="], "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], @@ -3185,21 +3256,19 @@ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], - "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="], - "js-string-escape": ["js-string-escape@1.0.1", "", {}, "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg=="], "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], @@ -3299,7 +3368,7 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], @@ -3311,12 +3380,8 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], @@ -3347,7 +3412,7 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], @@ -3359,13 +3424,13 @@ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "make-fetch-happen": ["make-fetch-happen@15.0.5", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg=="], + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], "mammoth": ["mammoth@1.12.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w=="], "map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="], - "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], @@ -3443,9 +3508,7 @@ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - - "mocha": ["mocha@11.7.5", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig=="], + "mocha": ["mocha@11.7.6", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", "ms": "^2.1.3", "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", "workerpool": "^9.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA=="], "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], @@ -3459,11 +3522,9 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], + "msgpackr": ["msgpackr@1.12.1", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ=="], - "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - - "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], @@ -3473,19 +3534,17 @@ "nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], + "next": ["next@16.2.9", "", { "dependencies": { "@next/env": "16.2.9", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.9", "@next/swc-darwin-x64": "16.2.9", "@next/swc-linux-arm64-gnu": "16.2.9", "@next/swc-linux-arm64-musl": "16.2.9", "@next/swc-linux-x64-gnu": "16.2.9", "@next/swc-linux-x64-musl": "16.2.9", "@next/swc-win32-arm64-msvc": "16.2.9", "@next/swc-win32-x64-msvc": "16.2.9", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww=="], - "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], + "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], @@ -3495,7 +3554,7 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-gyp": ["node-gyp@12.2.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ=="], + "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], @@ -3503,7 +3562,7 @@ "node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="], - "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], @@ -3533,13 +3592,13 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "nypm": ["nypm@0.6.7", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-s3ds97SD5pd1dULE+tHUk1DrV0cSHOnsfpcdGATJ8JpBo21DoKqN9exTH4/2nhPQNOLomBdTFMicN94S4DrZrQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -3551,9 +3610,9 @@ "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], @@ -3575,7 +3634,9 @@ "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], - "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], + "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="], + + "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], "oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="], @@ -3601,7 +3662,7 @@ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], - "pacote": ["pacote@21.5.0", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ=="], + "pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -3667,7 +3728,7 @@ "pkg-conf": ["pkg-conf@4.0.0", "", { "dependencies": { "find-up": "^6.0.0", "load-json-file": "^7.0.0" } }, "sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], @@ -3681,7 +3742,7 @@ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "poe-oauth": ["poe-oauth@0.0.6", "", {}, "sha512-dI8xrVl7RSFh0B+cb4GGuCjIfGtDT9VpbpVkP0UKcunpXF0eFw+6GencoJ7k+E02ZYqopBQApMVWGq70/GP69w=="], + "poe-oauth": ["poe-oauth@0.0.9", "", {}, "sha512-eT/f/1zE/Zk5zSLKz47LmPhYy7pDbUHIoFRIxjqlxLG1rmqQyYEo8V8bX4ObSFk5+qKEvW02t/9ZfSSW3VUcGw=="], "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], @@ -3689,19 +3750,19 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], + "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], - "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "posthog-js": ["posthog-js@1.369.3", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.25.2", "@posthog/types": "1.369.3", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-t4vk8mgkSdhIYr8YDRdLG45uJYH58MC7bPL83lKTEeDgoejyXbJ1/G77GZB/aWVQDST055GkgjQtUtK5DiYGkg=="], + "posthog-js": ["posthog-js@1.386.6", "", { "dependencies": { "@posthog/core": "^1.32.3", "@posthog/types": "^1.386.3", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-xRcbToRtU07Ojk+VaCCos6I/zD60sNKfWxdeZih0xbncgegIqLZJmBzi4HdkSkXrf14b6HhwMI/s2GxWha5ADQ=="], "posthog-node": ["posthog-node@4.4.0", "", { "dependencies": { "axios": "^1.7.4" } }, "sha512-4FEFBuc4FDFkTEiCSIo7DFV0jpprlqfR/SzjVgg7E2rRvfxTW4J47IvCwSCo5C9lBeV0ujDubY/YArHG37aJIw=="], "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], + "preact": ["preact@10.29.2", "", {}, "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], @@ -3729,13 +3790,13 @@ "promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], - "protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], + "protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -3747,7 +3808,7 @@ "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3771,11 +3832,11 @@ "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="], - "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -3837,11 +3898,13 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="], "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + "rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="], "rotating-file-stream": ["rotating-file-stream@3.2.9", "", {}, "sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug=="], @@ -3859,7 +3922,7 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="], + "safe-regex2": ["safe-regex2@5.1.1", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -3875,7 +3938,7 @@ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -3883,9 +3946,9 @@ "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], - "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -3907,11 +3970,11 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], "shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], @@ -3923,7 +3986,7 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "sigstore": ["sigstore@4.1.0", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.0", "@sigstore/tuf": "^4.0.1", "@sigstore/verify": "^3.1.0" } }, "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA=="], + "sigstore": ["sigstore@4.1.1", "", { "dependencies": { "@sigstore/bundle": "^4.0.0", "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/sign": "^4.1.1", "@sigstore/tuf": "^4.0.2", "@sigstore/verify": "^3.1.1" } }, "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -3945,7 +4008,7 @@ "socket.io-parser": ["socket.io-parser@4.2.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], - "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + "socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="], "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], @@ -4021,13 +4084,13 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + "strnum": ["strnum@2.4.0", "", { "dependencies": { "anynum": "^1.0.0" } }, "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg=="], "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], "supertap": ["supertap@3.0.1", "", { "dependencies": { "indent-string": "^5.0.0", "js-yaml": "^3.14.1", "serialize-error": "^7.0.1", "strip-ansi": "^7.0.1" } }, "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw=="], @@ -4037,28 +4100,22 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="], - "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], "table-layout": ["table-layout@4.1.1", "", { "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" } }, "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA=="], - "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + "tar": ["tar@7.5.16", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="], "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - - "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], - "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], @@ -4071,7 +4128,7 @@ "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], - "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], @@ -4081,21 +4138,21 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + "toad-cache": ["toad-cache@3.7.1", "", {}, "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -4117,7 +4174,7 @@ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="], @@ -4137,7 +4194,7 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -4145,14 +4202,12 @@ "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], - "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], + "typescript-eslint": ["typescript-eslint@8.61.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.0", "@typescript-eslint/parser": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0", "@typescript-eslint/utils": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw=="], "typical": ["typical@4.0.0", "", {}, "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw=="], "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "ulid": ["ulid@3.0.1", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q=="], "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], @@ -4197,8 +4252,6 @@ "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], - "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], @@ -4235,7 +4288,7 @@ "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], - "web-vitals": ["web-vitals@5.2.0", "", {}, "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA=="], + "web-vitals": ["web-vitals@5.3.0", "", {}, "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -4245,7 +4298,7 @@ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -4253,7 +4306,7 @@ "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], @@ -4271,7 +4324,7 @@ "write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], @@ -4279,6 +4332,8 @@ "xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }, "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="], @@ -4297,7 +4352,7 @@ "yargs-unparser": ["yargs-unparser@2.0.0", "", { "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", "flat": "^5.0.2", "is-plain-obj": "^2.1.0" } }, "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA=="], - "yauzl": ["yauzl@3.3.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ=="], + "yauzl": ["yauzl@3.4.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw=="], "yazl": ["yazl@2.5.1", "", { "dependencies": { "buffer-crc32": "~0.2.3" } }, "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw=="], @@ -4307,8 +4362,6 @@ "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], - "zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="], - "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], @@ -4327,11 +4380,27 @@ "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/deepgram/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HyuxddF2Yv5G8qxK/0uksAINjQ4h6TpwOqHuqzsCM0u78/JWAW2OXcIplQeB44PIAORgPjbMzrw9DhnPYHMskA=="], + + "@ai-sdk/fireworks/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], @@ -4365,14 +4434,14 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-n+PU8Z+gll7p3wDrH+Wo6fkt8sPrVnq30YYM6Ryga95oJlEneNMEbDHj0iqjMX3V7gaGdJo/hJWyPo4lscP+mA=="], + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.53", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/credential-provider-env": "^3.972.46", "@aws-sdk/credential-provider-http": "^3.972.48", "@aws-sdk/credential-provider-login": "^3.972.52", "@aws-sdk/credential-provider-process": "^3.972.46", "@aws-sdk/credential-provider-sso": "^3.972.52", "@aws-sdk/credential-provider-web-identity": "^3.972.52", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1066.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA=="], "@axe-core/playwright/axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], "@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -4385,13 +4454,13 @@ "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - "@effect/platform-node/undici": ["undici@8.1.0", "", {}, "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw=="], + "@effect/platform-node/undici": ["undici@8.4.1", "", {}, "sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "@eslint/eslintrc/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -4401,15 +4470,13 @@ "@gitlab/gitlab-ai-provider/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], - "@gitlab/gitlab-ai-provider/openai": ["openai@6.34.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw=="], - "@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@joshwooding/vite-plugin-react-docgen-typescript/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@kilocode/kilo-docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -4423,10 +4490,12 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@morphllm/morphsdk/ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="], - "@morphllm/morphsdk/openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], + "@npmcli/map-workspaces/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "@npmcli/package-json/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], "@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], @@ -4437,7 +4506,7 @@ "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], @@ -4457,63 +4526,63 @@ "@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="], + "@opencode-ai/core/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="], + + "@opencode-ai/core/@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="], + + "@opencode-ai/core/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="], + "@opencode-ai/core/@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], - "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-5k6lpgfIeLxvNhE8v3wEzdiu73ONKjF4gmH1AHvfqYd8kIVzQJai0KCDxgvqNncXHQhIWkaf1fg6+9hKaYJyaw=="], + "@opencode-ai/core/venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="], - "@opencode-ai/storybook/@storybook/addon-docs": ["@storybook/addon-docs@10.3.5", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.3.5", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.3.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA=="], + "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.4" } }, "sha512-/eUCx/6Ozq5grauwm/NqKtlW0oJ26b6GNesXrMuFID8WLg/qLEKf79Awfz9XrmyWxe7loD40K952r7AA5Oc23A=="], + + "@opencode-ai/storybook/@storybook/addon-docs": ["@storybook/addon-docs@10.4.4", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.4.4", "@storybook/icons": "^2.0.2", "@storybook/react-dom-shim": "10.4.4", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react"] }, "sha512-yPshCvtmQTq52T2sXuXgjy7B/QbhA/WIZxLYggptNjBL8BJMvbOfp9bAfCKh7+KpRWGqDZ6Y6tWL1Q48Wj3vtw=="], "@opencode-ai/storybook/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="], "@opencode-ai/storybook/react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], - "@opencode-ai/storybook/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "@opencode-ai/storybook/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/ui/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], + "@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + "@qdrant/js-client-rest/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], - "@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], + "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], - - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@qdrant/js-client-rest/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], - - "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="], - - "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="], + "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], "@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], - "@storybook/addon-links/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], - "@storybook/addon-onboarding/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], - "@storybook/addon-vitest/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "@storybook/addon-vitest/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], - "@storybook/builder-vite/@storybook/csf-plugin": ["@storybook/csf-plugin@10.3.5", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.3.5", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw=="], + "@storybook/builder-vite/@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.4", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.4", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w=="], - "@storybook/builder-vite/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], + "@storybook/builder-vite/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4537,8 +4606,6 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "@vscode/test-cli/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "@vscode/test-cli/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -4551,27 +4618,13 @@ "@vscode/vsce/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@vscode/vsce/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "@vscode/vsce/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "@vscode/vsce/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "@vscode/vsce/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "@vscode/vsce/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="], - - "ai-gateway-provider/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="], - - "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="], - - "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/google": "3.0.53", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xL44fHlTtDM7RLkMTgyqMfkfthA38JS91bbMaHItObIhte1PAIY936ZV1PLl/Z9A/oBAXjHWbXo5xDoHzB7LEg=="], - - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], - - "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.75", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V8UKK4fNpI9cnrtsZBvUp9O9J6Y9fTKBRoSLyEaNGPirACewixmLDbXsSgAeownPVWiWpK34bFysd+XouI5Ywg=="], - - "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], "ava/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -4587,27 +4640,33 @@ "ava/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "axios/proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], - "c8/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "c8/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "cacache/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cheerio/htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], + "cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="], + + "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -4635,21 +4694,15 @@ "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - - "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "effect/uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "esbuild-plugin-solid/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - - "esbuild-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], - - "eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4667,17 +4720,19 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "framer-motion/motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + "framer-motion/motion-dom": ["motion-dom@12.40.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], - "framer-motion/motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "friendly-words/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "friendly-words/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], "gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "gitlab-ai-provider/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], + "gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], - "gitlab-ai-provider/openai": ["openai@6.34.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw=="], + "gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "gitlab-ai-provider/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="], "gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -4709,7 +4764,7 @@ "kilo-code/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "knip/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "light-my-request/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], @@ -4745,13 +4800,13 @@ "mocha/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "node-gyp/undici": ["undici@6.26.0", "", {}, "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A=="], + "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "node-sarif-builder/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], @@ -4759,10 +4814,6 @@ "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "opentui-spinner/@opentui/core": ["@opentui/core@0.2.10", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.10", "@opentui/core-darwin-x64": "0.2.10", "@opentui/core-linux-arm64": "0.2.10", "@opentui/core-linux-x64": "0.2.10", "@opentui/core-win32-arm64": "0.2.10", "@opentui/core-win32-x64": "0.2.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oviCtx0jYjc7F8X2b8+0IkQLg6WH47Nwl6CFeZo5dU0k6OpSbTbi07ZleObaiECAp+S1YLhAtVdgzHU7hBZlaw=="], - - "opentui-spinner/@opentui/solid": ["@opentui/solid@0.2.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-+4/MB90yIQiPwg8Y4wY092yva9BvRTsJeeeEO3e2H7P8k8zxYk4G9bzuhqYLxA9mTVQ+zVDlrmFoPQhT7vpIRw=="], - "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -4779,14 +4830,10 @@ "pkg-conf/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], - "pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A=="], - "prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -4809,6 +4856,8 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "secretlint/globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], @@ -4849,32 +4898,28 @@ "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "tedious/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - "test-exclude/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "tree-sitter-bash/node-addon-api": ["node-addon-api@8.7.0", "", {}, "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA=="], + "tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "validate-npm-package-license/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "vite-plugin-icons-spritesheet/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "vite-plugin-solid/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - - "vite-plugin-solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], - "vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -4889,30 +4934,22 @@ "yargs-unparser/decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="], - "@ai-sdk/cerebras/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "@hey-api/openapi-ts/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], - "@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -4925,13 +4962,15 @@ "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/graphql/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], "@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4949,7 +4988,7 @@ "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], - "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.8", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw=="], + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.10", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" } }, "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w=="], "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], @@ -4957,15 +4996,39 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@opencode-ai/core/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="], + + "@opencode-ai/core/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@opencode-ai/core/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "@opencode-ai/core/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@opencode-ai/core/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + + "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="], + + "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], + + "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@opencode-ai/core/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="], "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ=="], - "@opencode-ai/storybook/@storybook/addon-docs/@storybook/csf-plugin": ["@storybook/csf-plugin@10.3.5", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.3.5", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw=="], + "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.29" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HyuxddF2Yv5G8qxK/0uksAINjQ4h6TpwOqHuqzsCM0u78/JWAW2OXcIplQeB44PIAORgPjbMzrw9DhnPYHMskA=="], - "@opencode-ai/storybook/@storybook/addon-docs/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.3.5", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.3.5" } }, "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g=="], + "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@opencode-ai/storybook/@storybook/addon-docs/react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + "@opencode-ai/core/venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.29", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA=="], + + "@opencode-ai/storybook/@storybook/addon-docs/@storybook/csf-plugin": ["@storybook/csf-plugin@10.4.4", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.4.4", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-1mzZyAwVUmAcw4WEUsJDVdSupkJf+Kf/f5uNAs4RzlBXA75P8YRkDKAb2EoMwsB5URiXFi9XoeAN/vWke0G6+w=="], + + "@opencode-ai/storybook/@storybook/addon-docs/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.4.4", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.4.4" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-y6SObmoW78AydE6VfKQSUmCkuqiaMPy9LgMpMdMEyWfJ/pSxBDMIKycr9dlRMJP1cvNgByaJgrusWtA46ndSQw=="], + + "@opencode-ai/storybook/@storybook/addon-docs/react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "@opencode-ai/storybook/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], @@ -4975,12 +5038,6 @@ "@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], @@ -5047,7 +5104,7 @@ "@vscode/test-cli/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@vscode/test-cli/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "@vscode/test-cli/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "@vscode/test-cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -5057,30 +5114,8 @@ "@vscode/vsce/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@vscode/vsce/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@vscode/vsce/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "@vscode/vsce/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], - - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="], - - "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], - - "ai-gateway-provider/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "ai-gateway-provider/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], - - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "ava/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "ava/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5095,6 +5130,8 @@ "ava/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -5115,6 +5152,8 @@ "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -5171,19 +5210,17 @@ "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "esbuild-plugin-solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "friendly-words/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "friendly-words/express/body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + "friendly-words/express/body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="], "friendly-words/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], @@ -5197,14 +5234,14 @@ "friendly-words/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], - "friendly-words/express/qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], - "friendly-words/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], "friendly-words/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], "friendly-words/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -5219,30 +5256,18 @@ "mocha/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "mocha/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "mocha/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "mocha/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "node-sarif-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "node-sarif-builder/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "node-sarif-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+lbDDj42Og+UtTZEwlHhGXichmOlkxSqn0J+Jqjat5/Tt5oZykj1NZjFIQ7ZSz4Miz7EmZwgYKE2CyOmmm9MoQ=="], - - "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iAoA0aqMWWAQ93nh8Bb0ipwt9h+tvEFc88+YO9St43uUJ+XrXcmMj3T8wtl6dSu/SN0UoDWNaUMHUmtykiPtg=="], - - "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-EnrkxgH5K76Oi/Br1UHPZblXG5P60snmtySfnxuVaeECNZrbTkV6BV/A0WoBeWshJweGbx1D+eTF+sEEjQCi8w=="], - - "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-fI+r3kCPqIxsWwPVGpKUQy4zHK8y+jkDRCwa3UbaUy48RQ44jMuf2RhVhmi4xmCvSc8UPJBbYsw1tLuh9kmXjg=="], - - "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-8F4z2hIRgkVWcr6CMVeJ9N4+1rmURPt2Pq2GBPko8ch6rxHR+a//KD1MfphyuLTHBS1tJ4vfZSWSoiaESImtrA=="], - - "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-Ki+qNBlIFW5K2wcG/RHrlPp7yEQKXeiNX3mlje25iwX62Ac5w391HBpOmUjbPoq20McPyDRnhbLfbXQSPtickg=="], - "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -5253,8 +5278,6 @@ "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="], - "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -5265,6 +5288,12 @@ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "rimraf/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "secretlint/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], "storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], @@ -5279,40 +5308,34 @@ "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], "test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "vite-plugin-solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], - "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], + "@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@opencode-ai/core/@aws-sdk/credential-providers/@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], @@ -5371,11 +5394,11 @@ "@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], @@ -5399,8 +5422,6 @@ "@vscode/vsce/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "@vscode/vsce/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "ava/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "ava/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -5409,7 +5430,7 @@ "ava/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5473,11 +5494,17 @@ "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "test-exclude/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5493,7 +5520,7 @@ "@storybook/builder-vite/storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@vscode/test-cli/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -5537,6 +5564,12 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "test-exclude/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "test-exclude/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -5553,6 +5586,10 @@ "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "rimraf/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "test-exclude/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "test-exclude/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/nix/hashes.json b/nix/hashes.json index 49ae8cbc98c..9bf86577db8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-qzU+msL1HicrkrYHNCitcutUqTFUatMArEuGgf/gOoE=", - "aarch64-linux": "sha256-GebxO5jgwnsNbvBdrbOlJ6u9C3/r6J7Q8WbzBv+wmhI=", - "aarch64-darwin": "sha256-NtAQ6QiL+iGaDxuFyvBiMFvBoEgBb5pK8b8xgkRdKx0=", - "x86_64-darwin": "sha256-rfxQmcEFsPD7AzSCBUZNxkNQUnkPnoN8a4shieEK1+Q=" + "x86_64-linux": "sha256-WYLCq59M4c6MsNjIeBmgV66v6LBvsTVGA1xV2oY935s=", + "aarch64-linux": "sha256-kpJ1SFCmBrhDwuJNJNoDU5NK6mDvEG4Yd41w9YJnsOs=", + "aarch64-darwin": "sha256-junSolEGOe+xUmPY4SAJGj+XKuytxvD0IWQWtLiaIEs=", + "x86_64-darwin": "sha256-p+3Jtb5CZexwx0UlhVeqeMr25R+jkjahFp9YTZYK0rE=" } } diff --git a/nix/kilo.nix b/nix/kilo.nix index 9d01a0ee26a..8ad056e09d7 100644 --- a/nix/kilo.nix +++ b/nix/kilo.nix @@ -3,6 +3,7 @@ stdenvNoCC, callPackage, bun, + bubblewrap, nodejs, sysctl, makeBinaryWrapper, @@ -39,6 +40,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json"; env.KILO_DISABLE_MODELS_FETCH = true; + env.KILO_SKIP_BUNDLED_BWRAP = "1"; env.KILO_VERSION = finalAttrs.version; env.KILO_CHANNEL = "local"; @@ -59,6 +61,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { install -Dm644 schema.json $out/share/kilo/schema.json wrapProgram $out/bin/kilo \ + ${lib.optionalString stdenvNoCC.hostPlatform.isLinux "--set KILO_BWRAP_PATH ${bubblewrap}/bin/bwrap"} \ --prefix PATH : ${ lib.makeBinPath ( [ @@ -97,7 +100,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { meta = { description = "AI-powered development tool"; homepage = "https://kilo.ai/"; - license = lib.licenses.mit; + license = [ lib.licenses.mit ] ++ lib.optional stdenvNoCC.hostPlatform.isLinux lib.licenses.lgpl2Plus; mainProgram = "kilo"; inherit (node_modules.meta) platforms; }; diff --git a/package.json b/package.json index 477bd4d7ec7..f725c678223 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,14 @@ "packages/sdk/js" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.65", - "@effect/platform-node": "4.0.0-beta.65", + "@effect/opentelemetry": "4.0.0-beta.66", + "@effect/platform-node": "4.0.0-beta.66", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.14", "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", - "@opentui/core": "0.2.11", - "@opentui/solid": "0.2.11", + "@opentui/core": "0.2.15", + "@opentui/solid": "0.2.15", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -48,9 +48,9 @@ "@tailwindcss/vite": "4.1.11", "diff": "8.0.4", "dompurify": "3.4.2", - "drizzle-kit": "1.0.0-beta.19-d95b7a4", - "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.65", + "drizzle-kit": "1.0.0-rc.2", + "drizzle-orm": "1.0.0-rc.2", + "effect": "4.0.0-beta.66", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.12.12", @@ -76,8 +76,9 @@ "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "solid-js": "1.9.12", "vite-plugin-solid": "2.11.10", - "@lydell/node-pty": "1.2.0-beta.10", - "@opentui/keymap": "0.2.11" + "@lydell/node-pty": "1.2.0-beta.12", + "@opentui/keymap": "0.2.15", + "@effect/sql-sqlite-bun": "4.0.0-beta.66" } }, "devDependencies": { @@ -147,6 +148,6 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.3.46", + "version": "7.3.54", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 8b327350796..8a56507dd3d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.46", + "version": "7.3.54", "name": "@opencode-ai/core", "type": "module", "license": "MIT", @@ -22,10 +22,12 @@ "@types/bun": "catalog:", "@types/cross-spawn": "catalog:", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3" + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:" }, "dependencies": { "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/sandbox": "workspace:*", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", "@npmcli/arborist": "9.4.0", @@ -45,15 +47,15 @@ "xdg-basedir": "5.1.0", "zod": "catalog:", "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.96", + "@ai-sdk/amazon-bedrock": "4.0.107", "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.54", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", - "@ai-sdk/google": "3.0.63", - "@ai-sdk/google-vertex": "4.0.112", + "@ai-sdk/google": "3.0.73", + "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.53", @@ -67,10 +69,11 @@ "@aws-sdk/credential-providers": "3.993.0", "@openrouter/ai-sdk-provider": "2.9.0", "ai-gateway-provider": "3.1.2", - "gitlab-ai-provider": "6.6.0", + "gitlab-ai-provider": "6.8.0", "google-auth-library": "10.5.0", "immer": "11.1.4", - "venice-ai-sdk-provider": "2.0.1" + "venice-ai-sdk-provider": "2.0.2", + "jsonc-parser": "3.3.1" }, "overrides": { "drizzle-orm": "catalog:" diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts new file mode 100644 index 00000000000..2d729086439 --- /dev/null +++ b/packages/core/src/account.ts @@ -0,0 +1,331 @@ +import path from "path" +import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect" +import { Identifier } from "./util/identifier" +import { NonNegativeInt, withStatics } from "./schema" +import { Global } from "./global" +import { AppFileSystem } from "./filesystem" +import { EventV2 } from "./event" + +export const ID = Schema.String.pipe( + Schema.brand("AccountV2.ID"), + withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })), +) +export type ID = typeof ID.Type + +export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID")) +export type ServiceID = typeof ServiceID.Type + +export class OAuthCredential extends Schema.Class("AccountV2.OAuthCredential")({ + type: Schema.Literal("oauth"), + refresh: Schema.String, + access: Schema.String, + expires: NonNegativeInt, + accountId: Schema.optional(Schema.String), // kilocode_change - preserve Kilo organization during v1 migration +}) {} + +export class ApiKeyCredential extends Schema.Class("AccountV2.ApiKeyCredential")({ + type: Schema.Literal("api"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) {} + +export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ + identifier: "AccountV2.Credential", + }) +export type Credential = Schema.Schema.Type + +export class Info extends Schema.Class("AccountV2.Info")({ + id: ID, + serviceID: ServiceID, + description: Schema.String, + credential: Credential, +}) {} + +export class FileWriteError extends Schema.TaggedErrorClass()("AccountV2.FileWriteError", { + operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]), + cause: Schema.Defect, +}) {} + +export type Error = FileWriteError + +export const Event = { + Added: EventV2.define({ + type: "account.added", + schema: { + account: Info, + }, + }), + Removed: EventV2.define({ + type: "account.removed", + schema: { + account: Info, + }, + }), + Switched: EventV2.define({ + type: "account.switched", + schema: { + serviceID: ServiceID, + from: Schema.optional(ID), + to: Schema.optional(ID), + }, + }), +} + +interface Writable { + version: 2 + accounts: Record + active: Record +} + +const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential)) + +function migrate(old: Record): Writable { + const accounts: Record = {} + const active: Record = {} + for (const [serviceID, value] of Object.entries(old)) { + const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({})) + const parsed = (decoded as Record)[serviceID] + if (!parsed) continue + const id = Identifier.ascending() + const account = ID.make(id) + const brandedServiceID = ServiceID.make(serviceID) + accounts[id] = new Info({ + id: account, + serviceID: brandedServiceID, + description: "default", + credential: parsed, + }) + active[brandedServiceID] = account + } + return { version: 2, accounts, active } +} + +export interface Interface { + readonly get: (id: ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly create: (input: { + serviceID: ServiceID + credential: Credential + description?: string + }) => Effect.Effect + readonly update: (id: ID, updates: Partial>) => Effect.Effect + readonly remove: (id: ID) => Effect.Effect + readonly activate: (id: ID) => Effect.Effect + readonly active: (serviceID: ServiceID) => Effect.Effect + readonly forService: (serviceID: ServiceID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Account") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fsys = yield* AppFileSystem.Service + const global = yield* Global.Service + const events = yield* EventV2.Service + const file = path.join(global.data, "account.json") + const legacyFile = path.join(global.data, "auth.json") + const prior = path.join(global.data, "auth-v2.json") // kilocode_change + + const writeMigrated = Effect.fnUntraced(function* (raw: Record) { + const migrated = migrate(raw) + yield* fsys + .writeJson(file, migrated, 0o600) + .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause }))) + return migrated + }) + + const parseAuthContent = () => { + try { + return JSON.parse(process.env.KILO_AUTH_CONTENT ?? "") + } catch {} + } + + const load: () => Effect.Effect = Effect.fnUntraced(function* () { + if (process.env.KILO_AUTH_CONTENT) { + const raw = parseAuthContent() + if (raw && typeof raw === "object") { + if ("version" in raw && raw.version === 2) return raw as Writable + return yield* writeMigrated(raw as Record) + } + return { version: 2, accounts: {}, active: {} } + } + + const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null)) + if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record) + + const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null)) + + if (raw && typeof raw === "object") { + if ("version" in raw && raw.version === 2) return raw as Writable + return yield* writeMigrated(raw as Record) + } + + // kilocode_change start - migrate the previous Kilo multi-account store after the current store + const previous = yield* fsys.readJson(prior).pipe(Effect.orElseSucceed(() => null)) + if (previous && typeof previous === "object" && "version" in previous && previous.version === 2) { + yield* fsys + .writeJson(file, previous, 0o600) + .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause }))) + return previous as Writable + } + // kilocode_change end + + return { version: 2, accounts: {}, active: {} } + }) + + const write = (data: Writable) => + fsys + .writeJson(file, data, 0o600) + .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "write", cause }))) + + const state = SynchronizedRef.makeUnsafe( + yield* load().pipe(Effect.orElseSucceed((): Writable => ({ version: 2, accounts: {}, active: {} }))), + ) + + const activate = Effect.fn("AccountV2.activate")(function* (id: ID) { + const data = yield* SynchronizedRef.get(state) + const account = data.accounts[id] + if (!account) return + const activated = yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const nextAccount = data.accounts[id] + if (!nextAccount) return [undefined, data] as const + + const next = { ...data, active: { ...data.active, [nextAccount.serviceID]: id } } + yield* write(next) + return [{ serviceID: nextAccount.serviceID, from: data.active[nextAccount.serviceID], to: id }, next] as const + }), + ) + if (activated) yield* events.publish(Event.Switched, activated) + }) + + const result: Interface = { + get: Effect.fn("AccountV2.get")(function* (id) { + return (yield* SynchronizedRef.get(state)).accounts[id] + }), + + all: Effect.fn("AccountV2.all")(function* () { + return Object.values((yield* SynchronizedRef.get(state)).accounts) + }), + + active: Effect.fn("AccountV2.active")(function* (serviceID) { + const data = yield* SynchronizedRef.get(state) + return ( + data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID) + ) + }), + + forService: Effect.fn("AccountV2.list")(function* (serviceID) { + return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID) + }), + + create: Effect.fn("AccountV2.add")(function* (input) { + const id = ID.make(Identifier.ascending()) + const account = new Info({ + id, + serviceID: input.serviceID, + description: input.description ?? "default", + credential: input.credential, + }) + const added = yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const next = { + ...data, + accounts: { ...data.accounts, [account.id]: account }, + active: { ...data.active, [account.serviceID]: account.id }, + } + + yield* write(next) + return [ + { + account, + switched: { serviceID: account.serviceID, from: data.active[account.serviceID], to: account.id }, + }, + next, + ] as const + }), + ) + yield* events.publish(Event.Added, { account: added.account }) + yield* events.publish(Event.Switched, added.switched) + return added.account + }), + + update: Effect.fn("AccountV2.update")(function* (id, updates) { + const existing = (yield* SynchronizedRef.get(state)).accounts[id] + if (!existing) return + yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + if (!data.accounts[id]) return [undefined, data] as const + + const next = { + ...data, + accounts: { + ...data.accounts, + [id]: new Info({ + id, + serviceID: existing.serviceID, + description: updates.description ?? existing.description, + credential: updates.credential ?? existing.credential, + }), + }, + } + + yield* write(next) + return [undefined, next] as const + }), + ) + }), + + remove: Effect.fn("AccountV2.remove")(function* (id) { + const removed = yield* SynchronizedRef.modifyEffect( + state, + Effect.fnUntraced(function* (data) { + const accounts = { ...data.accounts } + const active = { ...data.active } + const removed = accounts[id] + if (!removed) return [undefined, data] as const + const wasActive = active[removed.serviceID] === id + delete accounts[id] + const replacement = Object.values(accounts).find((account) => account.serviceID === removed.serviceID) + if (wasActive) { + if (replacement) active[removed.serviceID] = replacement.id + else delete active[removed.serviceID] + } + + const next = { ...data, accounts, active } + yield* write(next) + return [ + { + account: removed, + switched: wasActive ? { serviceID: removed.serviceID, from: id, to: replacement?.id } : undefined, + }, + next, + ] as const + }), + ) + if (removed) { + yield* events.publish(Event.Removed, { account: removed.account }) + if (removed.switched) yield* events.publish(Event.Switched, removed.switched) + } + }), + + activate, + } + + return Service.of(result) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Global.defaultLayer), + Layer.provide(EventV2.defaultLayer), +) + +export * as AccountV2 from "./account" diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts new file mode 100644 index 00000000000..ec7dfa2adeb --- /dev/null +++ b/packages/core/src/agent.ts @@ -0,0 +1,107 @@ +export * as AgentV2 from "./agent" + +import { Array, Context, Effect, Layer, Schema, Scope } from "effect" +import { castDraft, enableMapSet, type Draft } from "immer" +import { ModelV2 } from "./model" +import { PermissionV2 } from "./permission" +import { ProviderV2 } from "./provider" +import { PositiveInt } from "./schema" +import { State } from "./state" + +export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) +export type ID = typeof ID.Type + +export const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) + +export class Info extends Schema.Class("AgentV2.Info")({ + id: ID, + model: ModelV2.Ref.pipe(Schema.optional), + options: ProviderV2.Options, + system: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + mode: Schema.Literals(["subagent", "primary", "all"]), + hidden: Schema.Boolean, + color: Color.pipe(Schema.optional), + steps: PositiveInt.pipe(Schema.optional), + permissions: PermissionV2.Ruleset, +}) { + static empty(id: ID) { + return new Info({ + id, + options: { + headers: {}, + body: {}, + aisdk: { + provider: {}, + request: {}, + }, + }, + mode: "all", + hidden: false, + permissions: [], + }) + } +} + +type Data = { + agents: Map +} + +export type Editor = { + list: () => readonly Info[] + get: (id: ID) => Info | undefined + update: (id: ID, fn: (agent: Draft) => void) => void + remove: (id: ID) => void +} + +export interface Interface { + readonly transform: State.Interface["transform"] + readonly update: (update: State.Transform) => Effect.Effect + readonly get: (id: ID) => Effect.Effect + readonly all: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Agent") {} + +enableMapSet() + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = State.create({ + initial: () => ({ agents: new Map() }), + editor: (draft) => ({ + list: () => Array.fromIterable(draft.agents.values()) as Info[], + get: (id) => draft.agents.get(id), + update: (id, fn) => { + const current = draft.agents.get(id) ?? castDraft(Info.empty(id)) + if (!draft.agents.has(id)) draft.agents.set(id, current) + fn(current) + current.id = id + }, + remove: (id) => { + draft.agents.delete(id) + }, + }), + }) + + return Service.of({ + transform: state.transform, + update: Effect.fn("AgentV2.update")(function* (update) { + const transform = yield* state.transform() + yield* transform(update) + }), + get: Effect.fn("AgentV2.get")(function* (id) { + return state.get().agents.get(id) + }), + all: Effect.fn("AgentV2.all")(function* () { + return Array.fromIterable(state.get().agents.values()) + }), + }) + }), +) + +export const defaultLayer = layer diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts deleted file mode 100644 index 971b2320353..00000000000 --- a/packages/core/src/auth.ts +++ /dev/null @@ -1,265 +0,0 @@ -import path from "path" -import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect" -import { Identifier } from "./util/identifier" -import { NonNegativeInt, withStatics } from "./schema" -import { Global } from "./global" -import { AppFileSystem } from "./filesystem" - -export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" - -const AccountID = Schema.String.pipe( - Schema.brand("AccountID"), - withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })), -) -export type AccountID = typeof AccountID.Type - -export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID")) -export type ServiceID = typeof ServiceID.Type - -export class OAuthCredential extends Schema.Class("AuthV2.OAuthCredential")({ - type: Schema.Literal("oauth"), - refresh: Schema.String, - access: Schema.String, - expires: NonNegativeInt, - accountId: Schema.optional(Schema.String), // kilocode_change - preserve Kilo organization during v1 migration -}) {} - -export class ApiKeyCredential extends Schema.Class("AuthV2.ApiKeyCredential")({ - type: Schema.Literal("api"), - key: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), -}) {} - -export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ - identifier: "AuthV2.Credential", - }) -export type Credential = Schema.Schema.Type - -export class Account extends Schema.Class("AuthV2.Account")({ - id: AccountID, - serviceID: ServiceID, - description: Schema.String, - credential: Credential, -}) {} - -export class AuthFileWriteError extends Schema.TaggedErrorClass()("AuthV2.FileWriteError", { - operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]), - cause: Schema.Defect, -}) {} - -export type AuthError = AuthFileWriteError - -interface Writable { - version: 2 - accounts: Record - active: Record -} - -const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential)) - -function migrate(old: Record): Writable { - const accounts: Record = {} - const active: Record = {} - for (const [serviceID, value] of Object.entries(old)) { - const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({})) - const parsed = (decoded as Record)[serviceID] - if (!parsed) continue - const id = Identifier.ascending() - const accountID = AccountID.make(id) - const brandedServiceID = ServiceID.make(serviceID) - accounts[id] = new Account({ - id: accountID, - serviceID: brandedServiceID, - description: "default", - credential: parsed, - }) - active[brandedServiceID] = accountID - } - return { version: 2, accounts, active } -} - -export interface Interface { - readonly get: (accountID: AccountID) => Effect.Effect - readonly all: () => Effect.Effect - readonly create: (input: { - serviceID: ServiceID - credential: Credential - description?: string - active?: boolean - }) => Effect.Effect - readonly update: ( - accountID: AccountID, - updates: Partial>, - ) => Effect.Effect - readonly remove: (accountID: AccountID) => Effect.Effect - readonly activate: (accountID: AccountID) => Effect.Effect - readonly active: (serviceID: ServiceID) => Effect.Effect - readonly forService: (serviceID: ServiceID) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/Auth") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* AppFileSystem.Service - const global = yield* Global.Service - const file = path.join(global.data, "auth-v2.json") - const legacyFile = path.join(global.data, "auth.json") - - const writeMigrated = Effect.fnUntraced(function* (raw: Record) { - const migrated = migrate(raw) - yield* fsys - .writeJson(file, migrated, 0o600) - .pipe(Effect.mapError((cause) => new AuthFileWriteError({ operation: "migrate", cause }))) - return migrated - }) - - const parseAuthContent = () => { - try { - return JSON.parse(process.env.KILO_AUTH_CONTENT ?? "") - } catch {} - } - - const load: () => Effect.Effect = Effect.fnUntraced(function* () { - if (process.env.KILO_AUTH_CONTENT) { - const raw = parseAuthContent() - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - return { version: 2, accounts: {}, active: {} } - } - - const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null)) - if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record) - - const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null)) - - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - - return { version: 2, accounts: {}, active: {} } - }) - - const write = (data: Writable) => - fsys - .writeJson(file, data, 0o600) - .pipe(Effect.mapError((cause) => new AuthFileWriteError({ operation: "write", cause }))) - - const state = SynchronizedRef.makeUnsafe(yield* load()) - - const result: Interface = { - get: Effect.fn("AuthV2.get")(function* (accountID) { - return (yield* SynchronizedRef.get(state)).accounts[accountID] - }), - - all: Effect.fn("AuthV2.all")(function* () { - return Object.values((yield* SynchronizedRef.get(state)).accounts) - }), - - active: Effect.fn("AuthV2.active")(function* (serviceID) { - const data = yield* SynchronizedRef.get(state) - return ( - data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID) - ) - }), - - forService: Effect.fn("AuthV2.list")(function* (serviceID) { - return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID) - }), - - create: Effect.fn("AuthV2.add")(function* (input) { - return yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const account = new Account({ - id: AccountID.make(Identifier.ascending()), - serviceID: input.serviceID, - description: input.description ?? "default", - credential: input.credential, - }) - const next = { - ...data, - accounts: { ...data.accounts, [account.id]: account }, - active: - (input.active ?? Object.values(data.accounts).every((a) => a.serviceID !== input.serviceID)) - ? { ...data.active, [input.serviceID]: account.id } - : data.active, - } - - yield* write(next) - return [account, next] as const - }), - ) - }), - - update: Effect.fn("AuthV2.update")(function* (accountID, updates) { - yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const existing = data.accounts[accountID] - if (!existing) return [undefined, data] as const - - const next = { - ...data, - accounts: { - ...data.accounts, - [accountID]: new Account({ - id: accountID, - serviceID: existing.serviceID, - description: updates.description ?? existing.description, - credential: updates.credential ?? existing.credential, - }), - }, - } - - yield* write(next) - return [undefined, next] as const - }), - ) - }), - - remove: Effect.fn("AuthV2.remove")(function* (accountID) { - yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const accounts = { ...data.accounts } - const active = { ...data.active } - if (accounts[accountID] && active[accounts[accountID].serviceID] === accountID) - delete active[accounts[accountID].serviceID] - delete accounts[accountID] - - const next = { ...data, accounts, active } - yield* write(next) - return [undefined, next] as const - }), - ) - }), - - activate: Effect.fn("AuthV2.activate")(function* (accountID) { - yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const account = data.accounts[accountID] - if (!account) return [undefined, data] as const - - const next = { ...data, active: { ...data.active, [account.serviceID]: accountID } } - yield* write(next) - return [undefined, next] as const - }), - ) - }), - } - - return Service.of(result) - }), -) - -export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.defaultLayer)) - -export * as AuthV2 from "./auth" diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index d27f17bfb87..a12de5d476b 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,18 +1,22 @@ export * as Catalog from "./catalog" -import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array } from "effect" -import { produce, type Draft } from "immer" +import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect" +import { castDraft, enableMapSet, type Draft } from "immer" import { ModelV2 } from "./model" import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" import { Location } from "./location" import { EventV2 } from "./event" +import { Policy } from "./policy" +import { State } from "./state" -type ProviderRecord = { +export type ProviderRecord = { provider: ProviderV2.Info - models: HashMap.HashMap + models: Map } +export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } + export class ProviderNotFoundError extends Schema.TaggedErrorClass()( "CatalogV2.ProviderNotFound", { @@ -25,6 +29,8 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass + defaultModel?: DefaultModel +} + +export type Editor = { + provider: { + list: () => readonly ProviderRecord[] + get: (providerID: ProviderV2.ID) => ProviderRecord | undefined + update: (providerID: ProviderV2.ID, fn: (provider: Draft) => void) => void + remove: (providerID: ProviderV2.ID) => void + } + model: { + get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined + update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft) => void) => void + remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + default: { + get: () => DefaultModel | undefined + set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + } + } +} + export interface Interface { + readonly transform: State.Interface["transform"] readonly provider: { readonly get: (providerID: ProviderV2.ID) => Effect.Effect - readonly update: (providerID: ProviderV2.ID, fn: (provider: Draft) => void) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect } @@ -46,35 +75,28 @@ export interface Interface { providerID: ProviderV2.ID, modelID: ModelV2.ID, ) => Effect.Effect - readonly update: ( - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - fn: (model: Draft) => void, - ) => Effect.Effect readonly all: () => Effect.Effect readonly available: () => Effect.Effect readonly default: () => Effect.Effect> - readonly setDefault: ( - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - ) => Effect.Effect readonly small: (providerID: ProviderV2.ID) => Effect.Effect> } } export class Service extends Context.Service()("@opencode/v2/Catalog") {} +enableMapSet() + export const layer = Layer.effect( Service, Effect.gen(function* () { yield* Location.Service - let records = HashMap.empty() - let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined const plugin = yield* PluginV2.Service const events = yield* EventV2.Service + const policy = yield* Policy.Service + const scope = yield* Scope.Scope const resolve = (model: ModelV2.Info) => { - const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider + const provider = state.get().providers.get(model.providerID)!.provider const endpoint = model.endpoint.type === "unknown" ? provider.endpoint @@ -107,40 +129,97 @@ export const layer = Layer.effect( } function* getRecord(providerID: ProviderV2.ID) { - const match = HashMap.get(records, providerID) - if (!match.valueOrUndefined) return yield* new ProviderNotFoundError({ providerID }) - return match.value + const match = state.get().providers.get(providerID) + if (!match) return yield* new ProviderNotFoundError({ providerID }) + return match } + const normalizeEndpoint = (item: Draft | Draft) => { + if (item.endpoint.type !== "aisdk" || typeof item.options.aisdk.provider.baseURL !== "string") return + item.endpoint.url = item.options.aisdk.provider.baseURL + delete item.options.aisdk.provider.baseURL + } + + const state = State.create({ + initial: () => ({ providers: new Map() }), + editor: (draft) => { + const result: Editor = { + provider: { + list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[], + get: (providerID) => draft.providers.get(providerID), + update: (providerID, fn) => { + let current = draft.providers.get(providerID) + if (!current) { + current = castDraft({ + provider: ProviderV2.Info.empty(providerID), + models: new Map(), + }) + draft.providers.set(providerID, current) + } + fn(current.provider) + normalizeEndpoint(current.provider) + }, + remove: (providerID) => { + draft.providers.delete(providerID) + }, + }, + model: { + get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID), + update: (providerID, modelID, fn) => { + result.provider.update(providerID, () => {}) + const record = draft.providers.get(providerID)! + const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID)) + if (!record.models.has(modelID)) record.models.set(modelID, model) + fn(model) + model.id = modelID + model.providerID = providerID + normalizeEndpoint(model) + }, + remove: (providerID, modelID) => { + draft.providers.get(providerID)?.models.delete(modelID) + }, + default: { + get: () => draft.defaultModel, + set: (providerID, modelID) => { + draft.defaultModel = { providerID, modelID } + }, + }, + }, + } + return result + }, + finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) { + if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid) + for (const record of [...catalog.provider.list()]) { + if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") { + catalog.provider.remove(record.provider.id) + } + } + }), + }) + + yield* events.subscribe(PluginV2.Event.Added).pipe( + Stream.runForEach((event) => + state.update((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"), + ), + Effect.forkIn(scope, { startImmediately: true }), + ) + const result: Interface = { + transform: state.transform, + provider: { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { const record = yield* getRecord(providerID) return record.provider }), - update: Effect.fnUntraced(function* (providerID, fn) { - const current = Option.getOrUndefined(HashMap.get(records, providerID)) - const provider = produce(current?.provider ?? ProviderV2.Info.empty(providerID), (draft) => { - fn(draft) - if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") { - draft.endpoint.url = draft.options.aisdk.provider.baseURL - delete draft.options.aisdk.provider.baseURL - } - }) - const updated = yield* plugin.trigger("provider.update", {}, { provider, cancel: false }) - records = HashMap.set(records, providerID, { - provider: updated.provider, - models: current?.models ?? HashMap.empty(), - }) - }), - all: Effect.fn("CatalogV2.provider.all")(function* () { - return globalThis.Array.from(HashMap.values(records)).map((record) => record.provider) + return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) }), available: Effect.fn("CatalogV2.provider.available")(function* () { - return globalThis.Array.from(HashMap.values(records)) + return Array.fromIterable(state.get().providers.values()) .map((record) => record.provider) .filter((provider) => provider.enabled) }), @@ -149,39 +228,15 @@ export const layer = Layer.effect( model: { get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { const record = yield* getRecord(providerID) - const model = Option.getOrUndefined(HashMap.get(record.models, modelID)) + const model = record.models.get(modelID) if (!model) return yield* new ModelNotFoundError({ providerID, modelID }) return resolve(model) }), - update: Effect.fnUntraced(function* (providerID, modelID, fn) { - const record = yield* getRecord(providerID) - const model = produce( - HashMap.get(record.models, modelID).pipe(Option.getOrElse(() => ModelV2.Info.empty(providerID, modelID))), - (draft) => { - fn(draft) - if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") { - draft.endpoint.url = draft.options.aisdk.provider.baseURL - delete draft.options.aisdk.provider.baseURL - } - }, - ) - const updated = yield* plugin.trigger("model.update", {}, { model, cancel: false }) - if (updated.cancel) return - const next = new ModelV2.Info({ ...updated.model, id: modelID, providerID }) - records = HashMap.set(records, providerID, { - provider: record.provider, - models: HashMap.set(record.models, modelID, next), - }) - yield* events.publish(Event.ModelUpdated, { model: resolve(next) }) - return - }), - all: Effect.fn("CatalogV2.model.all")(function* () { return pipe( - records, - HashMap.toValues, - Array.flatMap((record) => HashMap.toValues(record.models)), + Array.fromIterable(state.get().providers.values()), + Array.flatMap((record) => Array.fromIterable(record.models.values())), Array.map(resolve), Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), ) @@ -189,12 +244,13 @@ export const layer = Layer.effect( available: Effect.fn("CatalogV2.model.available")(function* () { return (yield* result.model.all()).filter((model) => { - const record = Option.getOrUndefined(HashMap.get(records, model.providerID)) + const record = state.get().providers.get(model.providerID) return record?.provider.enabled !== false && model.enabled }) }), default: Effect.fn("CatalogV2.model.default")(function* () { + const defaultModel = state.get().defaultModel if (defaultModel) { const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option) if (Option.isSome(model) && model.value.enabled) return model @@ -207,22 +263,17 @@ export const layer = Layer.effect( ) }), - setDefault: Effect.fn("CatalogV2.model.setDefault")(function* (providerID, modelID) { - yield* result.model.get(providerID, modelID) - defaultModel = { providerID, modelID } - }), - small: Effect.fn("CatalogV2.model.small")(function* (providerID) { - const record = Option.getOrUndefined(HashMap.get(records, providerID)) + const record = state.get().providers.get(providerID) if (!record) return Option.none() if (providerID === ProviderV2.ID.opencode) { - const gpt5Nano = Option.getOrUndefined(HashMap.get(record.models, ModelV2.ID.make("gpt-5-nano"))) + const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano)) } const candidates = pipe( - HashMap.toValues(record.models), + Array.fromIterable(record.models.values()), Array.filter( (model) => model.providerID === providerID && @@ -266,4 +317,4 @@ export const layer = Layer.effect( const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ -export const defaultLayer = layer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer)) +export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer)) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts new file mode 100644 index 00000000000..c9e1396739e --- /dev/null +++ b/packages/core/src/config.ts @@ -0,0 +1,203 @@ +export * as Config from "./config" + +import path from "path" +import { type ParseError, parse } from "jsonc-parser" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { Location } from "./location" +import { PermissionV2 } from "./permission" +import { Policy } from "./policy" +import { AbsolutePath } from "./schema" +import { ConfigAgent } from "./config/agent" +import { ConfigAttachments } from "./config/attachments" +import { ConfigCompaction } from "./config/compaction" +import { ConfigExperimental } from "./config/experimental" +import { ConfigFormatter } from "./config/formatter" +import { ConfigLSP } from "./config/lsp" +import { ConfigMCP } from "./config/mcp" +import { ConfigPlugin } from "./config/plugin" +import { ConfigProvider } from "./config/provider" +import { ConfigReference } from "./config/reference" +import { ConfigToolOutput } from "./config/tool-output" +import { ConfigWatcher } from "./config/watcher" + +export class Info extends Schema.Class("Config.Info")({ + $schema: Schema.optional(Schema.String).annotate({ + description: "JSON schema reference for configuration validation", + }), + shell: Schema.String.pipe(Schema.optional).annotate({ + description: "Default shell to use for terminal and shell tool execution", + }), + model: Schema.String.pipe(Schema.optional).annotate({ + description: "Default model to use when no session or agent model is selected", + }), + autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) + .pipe(Schema.optional) + .annotate({ + description: "Automatically update or notify when a new version is available", + }), + share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ + description: "Control whether sessions may be shared manually, automatically, or not at all", + }), + enterprise: Schema.Struct({ + url: Schema.String.pipe(Schema.optional), + }) + .pipe(Schema.optional) + .annotate({ + description: "Enterprise sharing service configuration", + }), + username: Schema.String.pipe(Schema.optional).annotate({ + description: "Username displayed in conversations and used for telemetry identity", + }), + permissions: PermissionV2.Ruleset.pipe(Schema.optional).annotate({ + description: "Ordered tool permission rules applied to agent tool use", + }), + agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ + description: "Named built-in agent overrides and custom agent definitions", + }), + snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Enable snapshots used for undo and revert behavior", + }), + watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ + description: "Filesystem watcher configuration", + }), + formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in formatters or configure formatter overrides", + }), + lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in language servers or configure server overrides", + }), + attachments: ConfigAttachments.Info.pipe(Schema.optional).annotate({ + description: "Attachment processing configuration", + }), + tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ + description: "Tool output truncation thresholds", + }), + mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ + description: "MCP server configuration", + }), + compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ + description: "Conversation compaction behavior", + }), + skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs to discover skills from", + }), + instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs supplying ambient instructions", + }), + references: ConfigReference.Info.pipe(Schema.optional).annotate({ + description: "Named local directories or Git repositories available as external context", + }), + plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ + description: "Ordered external plugin packages to load", + }), + experimental: ConfigExperimental.Experimental.pipe(Schema.optional), + providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), +}) {} + +export const FileSource = Schema.Struct({ + type: Schema.Literal("file"), + path: Schema.String, +}).annotate({ identifier: "Config.FileSource" }) +export type FileSource = typeof FileSource.Type + +export const MemorySource = Schema.Struct({ + type: Schema.Literal("memory"), +}).annotate({ identifier: "Config.MemorySource" }) +export type MemorySource = typeof MemorySource.Type + +export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type")) +export type Source = typeof Source.Type + +export class Loaded extends Schema.Class("Config.Loaded")({ + source: Source, + info: Info, +}) {} + +export interface Interface { + /** Returns supplemental config directories from lowest to highest priority. */ + readonly directories: () => Effect.Effect + /** Loads location config files from lowest to highest priority. */ + readonly get: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Config") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const global = yield* Global.Service + const location = yield* Location.Service + const policy = yield* Policy.Service + const names = ["config.json", "opencode.json", "opencode.jsonc"] + + const loadFile = Effect.fnUntraced(function* (filepath: string) { + const text = yield* fs.readFileStringSafe(filepath) + if (!text) return + + const errors: ParseError[] = [] + const input: unknown = parse(text, errors, { allowTrailingComma: true }) + if (errors.length) return + + // Accept legacy fields while v2 is migrated incrementally; recognized + // fields still have to satisfy the v2 schema. + const info = Option.getOrUndefined( + Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }), + ) + if (!info) return + return new Loaded({ source: { type: "file", path: filepath }, info }) + }) + + const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { + return yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( + Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)), + ) + }) + + const globalDirectory = AbsolutePath.make(global.config) + const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config) + // Read configuration once when this location opens. Later calls reuse these + // values until the location is reopened. + const directories = locationIsGlobal + ? [globalDirectory] + : [ + globalDirectory, + ...(yield* fs + .up({ targets: [".opencode"], start: location.directory, stop: location.project.directory }) + .pipe(Effect.orDie)) + .toReversed() + .map((directory) => AbsolutePath.make(directory)), + ] + // A config closer to the opened directory should win over one higher up. + // Search starts nearby, so reverse the results before applying them. + const directPaths = locationIsGlobal + ? [] + : (yield* fs + .up({ targets: names.toReversed(), start: location.directory, stop: location.project.directory }) + .pipe(Effect.orDie)).toReversed() + const direct = yield* Effect.forEach(directPaths, loadFile).pipe( + Effect.orDie, + Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)), + ) + const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) + // Apply general settings first and more specific settings last: + // global config, project files, then `.opencode` files. + const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] + // Rules use the opposite order so a user-global rule can override a + // repository rule. Statement order inside each file stays unchanged. + yield* policy.load(configs.toReversed().flatMap((config) => config.info.experimental?.policies ?? [])) + + return Service.of({ + directories: Effect.fn("Config.directories")(function* () { + return directories + }), + get: Effect.fn("Config.get")(function* () { + return configs + }), + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.defaultLayer)) diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts new file mode 100644 index 00000000000..40d2bc94b58 --- /dev/null +++ b/packages/core/src/config/agent.ts @@ -0,0 +1,25 @@ +export * as ConfigAgent from "./agent" + +import { Schema } from "effect" +import { PermissionV2 } from "../permission" +import { ConfigProvider } from "./provider" +import { PositiveInt } from "../schema" + +export const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) + +export class Info extends Schema.Class("ConfigV2.Agent")({ + model: Schema.String.pipe(Schema.optional), + variant: Schema.String.pipe(Schema.optional), + options: ConfigProvider.Options.pipe(Schema.optional), + system: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), + color: Color.pipe(Schema.optional), + steps: PositiveInt.pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + permissions: PermissionV2.Ruleset.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/attachments.ts b/packages/core/src/config/attachments.ts new file mode 100644 index 00000000000..f14775ff3f7 --- /dev/null +++ b/packages/core/src/config/attachments.ts @@ -0,0 +1,15 @@ +export * as ConfigAttachments from "./attachments" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Image extends Schema.Class("ConfigV2.Attachments.Image")({ + auto_resize: Schema.Boolean.pipe(Schema.optional), + max_width: PositiveInt.pipe(Schema.optional), + max_height: PositiveInt.pipe(Schema.optional), + max_base64_bytes: PositiveInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Attachments")({ + image: Image.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts new file mode 100644 index 00000000000..eef67ee26a0 --- /dev/null +++ b/packages/core/src/config/compaction.ts @@ -0,0 +1,16 @@ +export * as ConfigCompaction from "./compaction" + +import { Schema } from "effect" +import { NonNegativeInt } from "../schema" + +export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ + turns: NonNegativeInt.pipe(Schema.optional), + tokens: NonNegativeInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Compaction")({ + auto: Schema.Boolean.pipe(Schema.optional), + prune: Schema.Boolean.pipe(Schema.optional), + keep: Keep.pipe(Schema.optional), + buffer: NonNegativeInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/experimental.ts b/packages/core/src/config/experimental.ts new file mode 100644 index 00000000000..12a02635db6 --- /dev/null +++ b/packages/core/src/config/experimental.ts @@ -0,0 +1,18 @@ +export * as ConfigExperimental from "./experimental" + +import { Schema } from "effect" +import { Catalog } from "../catalog" +import { Policy as PolicyV2 } from "../policy" + +// Each core domain exports the policy actions it supports. Adding an action to +// this union makes it valid in authored config while keeping Policy generic. +export const PolicyAction = Schema.Union([Catalog.PolicyActions]) + +export class Policy extends Schema.Class("ConfigV2.Experimental.Policy")({ + ...PolicyV2.Info.fields, + action: PolicyAction, +}) {} + +export class Experimental extends Schema.Class("ConfigV2.Experimental")({ + policies: Policy.pipe(Schema.Array, Schema.optional), +}) {} diff --git a/packages/core/src/config/formatter.ts b/packages/core/src/config/formatter.ts new file mode 100644 index 00000000000..e1f90302d1a --- /dev/null +++ b/packages/core/src/config/formatter.ts @@ -0,0 +1,12 @@ +export * as ConfigFormatter from "./formatter" + +import { Schema } from "effect" + +export class Entry extends Schema.Class("ConfigV2.Formatter.Entry")({ + disabled: Schema.Boolean.pipe(Schema.optional), + command: Schema.String.pipe(Schema.Array, Schema.optional), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), +}) {} + +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/lsp.ts b/packages/core/src/config/lsp.ts new file mode 100644 index 00000000000..651597befdd --- /dev/null +++ b/packages/core/src/config/lsp.ts @@ -0,0 +1,18 @@ +export * as ConfigLSP from "./lsp" + +import { Schema } from "effect" + +export const Disabled = Schema.Struct({ + disabled: Schema.Literal(true), +}) + +export class Server extends Schema.Class("ConfigV2.LSP.Server")({ + command: Schema.String.pipe(Schema.Array), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + env: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + initialization: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const Entry = Schema.Union([Disabled, Server]) +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts new file mode 100644 index 00000000000..fce853815b4 --- /dev/null +++ b/packages/core/src/config/mcp.ts @@ -0,0 +1,36 @@ +export * as ConfigMCP from "./mcp" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Local extends Schema.Class("ConfigV2.MCP.Local")({ + type: Schema.Literal("local"), + command: Schema.String.pipe(Schema.Array), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: PositiveInt.pipe(Schema.optional), +}) {} + +export class OAuth extends Schema.Class("ConfigV2.MCP.OAuth")({ + client_id: Schema.String.pipe(Schema.optional), + client_secret: Schema.String.pipe(Schema.optional), + scope: Schema.String.pipe(Schema.optional), + callback_port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })).pipe(Schema.optional), + redirect_uri: Schema.String.pipe(Schema.optional), +}) {} + +export class Remote extends Schema.Class("ConfigV2.MCP.Remote")({ + type: Schema.Literal("remote"), + url: Schema.String, + headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: PositiveInt.pipe(Schema.optional), +}) {} + +export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type")) + +export class Info extends Schema.Class("ConfigV2.MCP")({ + timeout: PositiveInt.pipe(Schema.optional), + servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/plugin.ts b/packages/core/src/config/plugin.ts new file mode 100644 index 00000000000..e5fd6661ff5 --- /dev/null +++ b/packages/core/src/config/plugin.ts @@ -0,0 +1,13 @@ +export * as ConfigPlugin from "./plugin" + +import { Schema } from "effect" + +export class Entry extends Schema.Class("ConfigV2.Plugin.Entry")({ + package: Schema.String, + options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const Plugin = Schema.Union([Schema.String, Entry]) +export type Plugin = typeof Plugin.Type + +export const Plugins = Plugin.pipe(Schema.Array) diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts new file mode 100644 index 00000000000..c05b0a578f0 --- /dev/null +++ b/packages/core/src/config/plugin/agent.ts @@ -0,0 +1,65 @@ +export * as ConfigAgentPlugin from "./agent" + +import { Effect } from "effect" +import { AgentV2 } from "../../agent" +import { Config } from "../../config" +import { ModelV2 } from "../../model" +import { PermissionV2 } from "../../permission" +import { PluginV2 } from "../../plugin" + +export const Plugin = PluginV2.define({ + id: PluginV2.ID.make("config-agent"), + effect: Effect.gen(function* () { + const agent = yield* AgentV2.Service + const config = yield* Config.Service + const files = yield* config.get() + + yield* agent.update((editor) => { + const permissions = new Map() + + for (const file of files) { + for (const [id, item] of Object.entries(file.info.agents ?? {})) { + const agentID = AgentV2.ID.make(id) + if (item.disabled) { + editor.remove(agentID) + permissions.delete(agentID) + continue + } + + editor.update(agentID, (agent) => { + if (item.model !== undefined) { + const model = ModelV2.parse(item.model) + agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } + } + if (item.variant !== undefined && agent.model !== undefined) { + agent.model.variant = ModelV2.VariantID.make(item.variant) + } + if (item.options !== undefined) { + Object.assign(agent.options.headers, item.options.headers ?? {}) + Object.assign(agent.options.body, item.options.body ?? {}) + Object.assign(agent.options.aisdk.provider, item.options.aisdk?.provider ?? {}) + Object.assign(agent.options.aisdk.request, item.options.aisdk?.request ?? {}) + } + if (item.system !== undefined) agent.system = item.system + if (item.description !== undefined) agent.description = item.description + if (item.mode !== undefined) agent.mode = item.mode + if (item.hidden !== undefined) agent.hidden = item.hidden + if (item.color !== undefined) agent.color = item.color + if (item.steps !== undefined) agent.steps = item.steps + }) + + if (item.permissions !== undefined) { + permissions.set(agentID, [...(permissions.get(agentID) ?? []), ...item.permissions]) + } + } + } + + const global = files.flatMap((file) => file.info.permissions ?? []) + for (const current of editor.list()) { + editor.update(current.id, (agent) => { + agent.permissions.push(...global, ...(permissions.get(current.id) ?? [])) + }) + } + }) + }), +}) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts new file mode 100644 index 00000000000..fca2e53302e --- /dev/null +++ b/packages/core/src/config/plugin/provider.ts @@ -0,0 +1,95 @@ +export * as ConfigProviderPlugin from "./provider" + +import { Effect } from "effect" +import { Catalog } from "../../catalog" +import { Config } from "../../config" +import { ModelV2 } from "../../model" +import { PluginV2 } from "../../plugin" +import { ProviderV2 } from "../../provider" + +export const Plugin = PluginV2.define({ + id: PluginV2.ID.make("config-provider"), + effect: Effect.gen(function* () { + const catalog = yield* Catalog.Service + const config = yield* Config.Service + const transform = yield* catalog.transform() + const files = yield* config.get() + + yield* transform((catalog) => { + for (const file of files) { + for (const [id, item] of Object.entries(file.info.providers ?? {})) { + const providerID = ProviderV2.ID.make(id) + catalog.provider.update(providerID, (provider) => { + if (item.name !== undefined) provider.name = item.name + if (item.env !== undefined) provider.env = [...item.env] + provider.enabled = { via: "custom", data: {} } + if (item.endpoint !== undefined) provider.endpoint = { ...item.endpoint } + if (item.options !== undefined) { + Object.assign(provider.options.headers, item.options.headers ?? {}) + Object.assign(provider.options.body, item.options.body ?? {}) + Object.assign(provider.options.aisdk.provider, item.options.aisdk?.provider ?? {}) + Object.assign(provider.options.aisdk.request, item.options.aisdk?.request ?? {}) + } + }) + + for (const [id, config] of Object.entries(item.models ?? {})) { + catalog.model.update(providerID, ModelV2.ID.make(id), (model) => { + if (config.api_id !== undefined) model.apiID = config.api_id + if (config.family !== undefined) model.family = config.family + if (config.name !== undefined) model.name = config.name + if (config.endpoint !== undefined) model.endpoint = { ...config.endpoint } + if (config.capabilities !== undefined) { + model.capabilities = { + tools: config.capabilities.tools, + input: [...config.capabilities.input], + output: [...config.capabilities.output], + } + } + if (config.options !== undefined) { + Object.assign(model.options.headers, config.options.headers ?? {}) + Object.assign(model.options.body, config.options.body ?? {}) + Object.assign(model.options.aisdk.provider, config.options.aisdk?.provider ?? {}) + Object.assign(model.options.aisdk.request, config.options.aisdk?.request ?? {}) + if (config.options.variant !== undefined) model.options.variant = config.options.variant + } + if (config.variants !== undefined) { + for (const variant of config.variants) { + let existing = model.variants.find((item) => item.id === variant.id) + if (!existing) { + existing = { + id: variant.id, + headers: {}, + body: {}, + aisdk: { + provider: {}, + request: {}, + }, + } + model.variants.push(existing) + } + Object.assign(existing.headers, variant.headers ?? {}) + Object.assign(existing.body, variant.body ?? {}) + Object.assign(existing.aisdk.provider, variant.aisdk?.provider ?? {}) + Object.assign(existing.aisdk.request, variant.aisdk?.request ?? {}) + } + } + if (config.cost !== undefined) { + model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({ + tier: cost.tier && { ...cost.tier }, + input: cost.input, + output: cost.output, + cache: { + read: cost.cache?.read ?? 0, + write: cost.cache?.write ?? 0, + }, + })) + } + if (config.disabled !== undefined) model.enabled = !config.disabled + if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit } + }) + } + } + } + }) + }), +}) diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts new file mode 100644 index 00000000000..fbb0e1c3ef2 --- /dev/null +++ b/packages/core/src/config/provider.ts @@ -0,0 +1,62 @@ +export * as ConfigProvider from "./provider" + +import { Schema } from "effect" +import { ProviderV2 } from "../provider" +import { ModelV2 } from "../model" + +export class Options extends Schema.Class("ConfigV2.Provider.Options")({ + headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + aisdk: Schema.Struct({ + provider: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + request: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + }).pipe(Schema.optional), +}) {} + +class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ + read: Schema.Finite.pipe(Schema.optional), + write: Schema.Finite.pipe(Schema.optional), +}) {} + +class Cost extends Schema.Class("ConfigV2.Model.Cost")({ + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Int, + }).pipe(Schema.optional), + input: Schema.Finite, + output: Schema.Finite, + cache: Cache.pipe(Schema.optional), +}) {} + +class Limit extends Schema.Class("ConfigV2.Model.Limit")({ + context: Schema.Int.pipe(Schema.optional), + input: Schema.Int.pipe(Schema.optional), + output: Schema.Int.pipe(Schema.optional), +}) {} + +class Model extends Schema.Class("ConfigV2.Model")({ + api_id: ModelV2.ID.pipe(Schema.optional), + family: ModelV2.Family.pipe(Schema.optional), + name: Schema.String.pipe(Schema.optional), + endpoint: ProviderV2.Endpoint.pipe(Schema.optional), + capabilities: ModelV2.Capabilities.pipe(Schema.optional), + options: Schema.Struct({ + ...Options.fields, + variant: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional), + variants: Schema.Struct({ + id: ModelV2.VariantID, + ...Options.fields, + }).pipe(Schema.Array, Schema.optional), + cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + limit: Limit.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Provider")({ + name: Schema.String.pipe(Schema.optional), + env: Schema.String.pipe(Schema.Array, Schema.optional), + endpoint: ProviderV2.Endpoint.pipe(Schema.optional), + options: Options.pipe(Schema.optional), + models: Schema.Record(Schema.String, Model).pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts new file mode 100644 index 00000000000..dc9042e6f76 --- /dev/null +++ b/packages/core/src/config/reference.ts @@ -0,0 +1,17 @@ +export * as ConfigReference from "./reference" + +import { Schema } from "effect" + +export class Git extends Schema.Class("ConfigV2.Reference.Git")({ + repository: Schema.String, + branch: Schema.String.pipe(Schema.optional), +}) {} + +export class Local extends Schema.Class("ConfigV2.Reference.Local")({ + path: Schema.String, +}) {} + +export const Entry = Schema.Union([Schema.String, Git, Local]) +export type Entry = typeof Entry.Type + +export const Info = Schema.Record(Schema.String, Entry) diff --git a/packages/core/src/config/tool-output.ts b/packages/core/src/config/tool-output.ts new file mode 100644 index 00000000000..53e4d4d088b --- /dev/null +++ b/packages/core/src/config/tool-output.ts @@ -0,0 +1,9 @@ +export * as ConfigToolOutput from "./tool-output" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Info extends Schema.Class("ConfigV2.ToolOutput")({ + max_lines: PositiveInt.pipe(Schema.optional), + max_bytes: PositiveInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/watcher.ts b/packages/core/src/config/watcher.ts new file mode 100644 index 00000000000..2df6c876bfc --- /dev/null +++ b/packages/core/src/config/watcher.ts @@ -0,0 +1,7 @@ +export * as ConfigWatcher from "./watcher" + +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigV2.Watcher")({ + ignore: Schema.String.pipe(Schema.Array, Schema.optional), +}) {} diff --git a/packages/core/src/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts index ad8d4126d45..5c769569e63 100644 --- a/packages/core/src/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -1,6 +1,7 @@ import type * as Arr from "effect/Array" import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node" import * as NodePath from "@effect/platform-node/NodePath" +import { prepareCommand as prepareSandbox } from "@kilocode/sandbox" // kilocode_change import * as Deferred from "effect/Deferred" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" @@ -362,24 +363,29 @@ export const make = Effect.gen(function* () { function* (command) { switch (command._tag) { case "StandardCommand": { - const sin = stdin(command.options) - const sout = stdio(command.options, "stdout") - const serr = stdio(command.options, "stderr") - const extra = fds(command.options) const dir = yield* cwd(command.options) + // kilocode_change start - prepare agent-scoped commands through the selected sandbox backend + const target = yield* prepareSandbox(command, dir, env(command.options)) + const sin = stdin(target.options) + const sout = stdio(target.options, "stdout") + const serr = stdio(target.options, "stderr") + const extra = fds(target.options) + // kilocode_change end const [proc, signal] = yield* Effect.acquireRelease( - spawn(command, { + // kilocode_change start - spawn the prepared command and options + spawn(target, { cwd: dir, - env: env(command.options), + env: env(target.options), stdio: stdios(sin, sout, serr, extra), - detached: command.options.detached ?? process.platform !== "win32", - shell: command.options.shell, + detached: target.options.detached ?? process.platform !== "win32", + shell: target.options.shell, + // kilocode_change end windowsHide: process.platform === "win32", }), Effect.fnUntraced(function* ([proc, signal]) { const done = yield* Deferred.isDone(signal) - const kill = timeout(proc, command, command.options) + const kill = timeout(proc, command, target.options) // kilocode_change if (done) { const [code] = yield* Deferred.await(signal) if (process.platform === "win32") return yield* Effect.void @@ -388,14 +394,16 @@ export const make = Effect.gen(function* () { } const send = (s: NodeJS.Signals) => Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s)) - const sig = command.options.killSignal ?? "SIGTERM" + // kilocode_change start - preserve kill options from the prepared command + const sig = target.options.killSignal ?? "SIGTERM" const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid) - const escalated = command.options.forceKillAfter + const escalated = target.options.forceKillAfter ? Effect.timeoutOrElse(attempt, { - duration: command.options.forceKillAfter, + duration: target.options.forceKillAfter, orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid), }) : attempt + // kilocode_change end return yield* Effect.ignore(escalated) }), ) diff --git a/packages/opencode/src/effect/service-use.ts b/packages/core/src/effect/service-use.ts similarity index 85% rename from packages/opencode/src/effect/service-use.ts rename to packages/core/src/effect/service-use.ts index a93cdecbb15..89d9acf98ea 100644 --- a/packages/opencode/src/effect/service-use.ts +++ b/packages/core/src/effect/service-use.ts @@ -15,6 +15,7 @@ type ServiceUse = { } export const serviceUse = (tag: Context.Service) => { + const cache = new Map Effect.Effect>() // This is the only dynamic boundary: TypeScript knows the accessor shape, // but Proxy property names are runtime values. const access = new Proxy( @@ -22,7 +23,9 @@ export const serviceUse = (tag: Context.Service { if (typeof key !== "string") return undefined - return (...args: unknown[]) => + const cached = cache.get(key) + if (cached) return cached + const accessor = (...args: unknown[]) => tag.use((service) => { // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime. const method = service[key as keyof Shape] @@ -30,6 +33,8 @@ export const serviceUse = (tag: Context.Service Effect.Effect)(...args) }) + cache.set(key, accessor) + return accessor }, }, ) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index e01dc5b0d63..339fbddecf1 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -1,3 +1,5 @@ +export * as EventV2 from "./event" + import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Location } from "./location" import { withStatics } from "./schema" @@ -126,7 +128,7 @@ export const layer = Layer.effect( ...(options?.metadata ? { metadata: options.metadata } : {}), type: definition.type, ...(definition.version === undefined ? {} : { version: definition.version }), - ...(location ? { location } : {}), + ...(location ? { location: { directory: location.directory, workspaceID: location.workspaceID } } : {}), data, } as Payload return yield* publishEvent(event) @@ -153,5 +155,3 @@ export const layer = Layer.effect( ) export const defaultLayer = layer - -export * as EventV2 from "./event" diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 2e7a450d9b4..758e60e78ee 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -1,33 +1,13 @@ import { NodeFileSystem } from "@effect/platform-node" +import { decorateFileSystem, ensureDirectory } from "@kilocode/sandbox" // kilocode_change import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path" // kilocode_change - harden containment checks import { realpathSync } from "fs" import * as NFS from "fs/promises" import { lookup } from "mime-types" -import { Effect, FileSystem, Layer, Schema, Context } from "effect" +import { Context, Effect, FileSystem, Layer, Schema } from "effect" import type { PlatformError } from "effect/PlatformError" import { Glob } from "./util/glob" - -// kilocode_change start - Windows-resilient mkdir -p. -// fs.mkdir(dir, { recursive: true }) should be idempotent, but on Windows -// with NTFS reparse points (OneDrive), directory junctions, or WSL-served -// paths, libuv can still throw EEXIST. This wrapper catches that specific -// error so callers get the promised directory-exists semantics. -// -// https://github.com/Kilo-Org/kilocode/issues/9618 -// https://github.com/Kilo-Org/kilocode/issues/9755 -function isEexist(err: unknown): boolean { - return typeof err === "object" && err !== null && "code" in err && (err as NodeJS.ErrnoException).code === "EEXIST" -} - -async function mkdirSafe(dir: string): Promise { - try { - await NFS.mkdir(dir, { recursive: true }) - } catch (err: unknown) { - if (isEexist(err)) return - throw err - } -} -// kilocode_change end +import { serviceUse } from "./effect/service-use" export namespace AppFileSystem { export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", { @@ -61,10 +41,12 @@ export namespace AppFileSystem { export class Service extends Context.Service()("@opencode/FileSystem") {} + export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem + const fs = decorateFileSystem(yield* FileSystem.FileSystem) // kilocode_change const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) { return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)) @@ -113,12 +95,7 @@ export namespace AppFileSystem { }) const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { - // kilocode_change start - use mkdirSafe to tolerate Windows EEXIST - yield* Effect.tryPromise({ - try: () => mkdirSafe(path), - catch: (cause) => new FileSystemError({ method: "ensureDir", cause }), - }) - // kilocode_change end + yield* ensureDirectory(fs, path) // kilocode_change - mutate through the sandbox-confined filesystem }) const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( @@ -133,12 +110,7 @@ export namespace AppFileSystem { (e) => e.reason._tag === "NotFound", () => Effect.gen(function* () { - // kilocode_change start - use mkdirSafe to tolerate Windows EEXIST - yield* Effect.tryPromise({ - try: () => mkdirSafe(dirname(path)), - catch: (cause) => new FileSystemError({ method: "writeWithDirs:mkdir", cause }), - }) - // kilocode_change end + yield* ensureDirectory(fs, dirname(path)) // kilocode_change - sandbox-confined mkdir yield* write }), ), diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 0ed6eb47700..58ec9713629 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -28,6 +28,10 @@ const KILO_DISABLE_CLAUDE_CODE = truthy("KILO_DISABLE_CLAUDE_CODE") const KILO_DISABLE_CLAUDE_CODE_SKILLS = KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_SKILLS") const copy = process.env["KILO_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] +function enabledByExperimental(key: string) { + return process.env[key] === undefined ? KILO_EXPERIMENTAL : truthy(key) +} + export const Flag = { OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], @@ -42,7 +46,6 @@ export const Flag = { KILO_DISABLE_PRUNE: truthy("KILO_DISABLE_PRUNE"), KILO_DISABLE_TERMINAL_TITLE: truthy("KILO_DISABLE_TERMINAL_TITLE"), KILO_SHOW_TTFD: truthy("KILO_SHOW_TTFD"), - KILO_PERMISSION: process.env["KILO_PERMISSION"], KILO_DISABLE_DEFAULT_PLUGINS: truthy("KILO_DISABLE_DEFAULT_PLUGINS"), KILO_DISABLE_LSP_DOWNLOAD: truthy("KILO_DISABLE_LSP_DOWNLOAD"), KILO_ENABLE_EXPERIMENTAL_MODELS: truthy("KILO_ENABLE_EXPERIMENTAL_MODELS"), @@ -86,10 +89,10 @@ export const Flag = { KILO_STRICT_CONFIG_DEPS: truthy("KILO_STRICT_CONFIG_DEPS"), KILO_WORKSPACE_ID: process.env["KILO_WORKSPACE_ID"], - KILO_EXPERIMENTAL_WORKSPACES: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_WORKSPACES"), + KILO_EXPERIMENTAL_WORKSPACES: enabledByExperimental("KILO_EXPERIMENTAL_WORKSPACES"), KILO_EXPERIMENTAL_EVENT_SYSTEM: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EVENT_SYSTEM"), KILO_EXPERIMENTAL_SESSION_SWITCHING: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_SESSION_SWITCHING"), - + KILO_EXPERIMENTAL_SESSION_SWITCHER: enabledByExperimental("KILO_EXPERIMENTAL_SESSION_SWITCHER"), get KILO_DISABLE_PROJECT_CONFIG() { return truthy("KILO_DISABLE_PROJECT_CONFIG") }, @@ -102,6 +105,9 @@ export const Flag = { get KILO_PURE() { return truthy("KILO_PURE") }, + get KILO_PERMISSION() { + return process.env["KILO_PERMISSION"] + }, get KILO_PLUGIN_META_FILE() { return process.env["KILO_PLUGIN_META_FILE"] }, diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts new file mode 100644 index 00000000000..a5745233686 --- /dev/null +++ b/packages/core/src/git.ts @@ -0,0 +1,114 @@ +export * as Git from "./git" + +import path from "path" +import { Context, Effect, Layer } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { AbsolutePath } from "./schema" +import { AppFileSystem } from "./filesystem" +import { AppProcess } from "./process" + +export interface Repo { + /** + * The root directory of the working tree that contains the input path. + * + * For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`. + * For `/home/me/app-feature/src/file.ts` in a linked worktree, this is + * `/home/me/app-feature`. + */ + readonly directory: AbsolutePath + /** + * The shared Git storage directory used by this repo and any linked worktrees. + * + * For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`. + * For a linked worktree at `/home/me/app-feature` whose main checkout is + * `/home/me/app`, this is usually `/home/me/app/.git`. + */ + readonly store: AbsolutePath +} + +export interface Interface { + readonly find: (input: AbsolutePath) => Effect.Effect + readonly remote: (repo: Repo, name?: string) => Effect.Effect + readonly roots: (repo: Repo) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/GitV2") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const proc = yield* AppProcess.Service + + const find = Effect.fn("Git.find")(function* (input: AbsolutePath) { + const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe( + Effect.map((matches) => matches[0]), + Effect.catch(() => Effect.succeed(undefined)), + ) + if (!dotgit) return undefined + + const cwd = path.dirname(dotgit) + const git = run(cwd, proc) + const topLevel = yield* git(["rev-parse", "--show-toplevel"]) + const commonDir = yield* git(["rev-parse", "--git-common-dir"]) + if (commonDir.exitCode !== 0) return undefined + + return { + directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd), + store: AbsolutePath.make(resolvePath(cwd, commonDir.text)), + } satisfies Repo + }) + + const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") { + const result = yield* run(repo.directory, proc)(["remote", "get-url", name]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const roots = Effect.fn("Git.roots")(function* (repo: Repo) { + const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"]) + if (result.exitCode !== 0) return [] + return result.text + .split("\n") + .map((item) => item.trim()) + .filter(Boolean) + .toSorted() + }) + + return Service.of({ find, remote, roots }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(AppProcess.defaultLayer), +) + +interface Result { + readonly exitCode: number + readonly text: string +} + +function run(cwd: string, proc: AppProcess.Interface) { + return (args: string[]) => + proc + .run( + ChildProcess.make("git", args, { + cwd, + extendEnv: true, + stdin: "ignore", + }), + ) + .pipe( + Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result), + Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)), + ) +} + +function resolvePath(cwd: string, value: string) { + const trimmed = value.replace(/[\r\n]+$/, "") + if (!trimmed) return cwd + const normalized = AppFileSystem.windowsPath(trimmed) + if (path.isAbsolute(normalized)) return path.normalize(normalized) + return path.resolve(cwd, normalized) +} diff --git a/packages/core/src/location-layer.ts b/packages/core/src/location-layer.ts index 84dfb3dfae7..67293f7c5f9 100644 --- a/packages/core/src/location-layer.ts +++ b/packages/core/src/location-layer.ts @@ -2,11 +2,17 @@ import { Layer, LayerMap } from "effect" import { Location } from "./location" import { Catalog } from "./catalog" import { PluginBoot } from "./plugin/boot" +import { Policy } from "./policy" +import { Config } from "./config" export class LocationServiceMap extends LayerMap.Service()("@opencode/example/LocationServiceMap", { lookup: (ref: Location.Ref) => { - const location = Layer.succeed(Location.Service, Location.Service.of(ref)) - return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(location)) + const result = Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer, Config.defaultLayer).pipe( + Layer.provideMerge(Policy.defaultLayer), + Layer.provideMerge(Location.defaultLayer(ref)), + ) + return result }, - idleTimeToLive: "5 minutes", + idleTimeToLive: "60 minutes", + dependencies: [], }) {} diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index 00ff9cd3ea7..68c9a8f791e 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -1,11 +1,40 @@ -import { Context, Schema } from "effect" +import { Context, Effect, Layer, Schema } from "effect" +import { Project } from "./project" +import { AbsolutePath } from "./schema" export * as Location from "./location" export const Ref = Schema.Struct({ - directory: Schema.String, + directory: AbsolutePath, workspaceID: Schema.optional(Schema.String), }).annotate({ identifier: "Location.Ref" }) export type Ref = typeof Ref.Type -export class Service extends Context.Service()("@opencode/Location") {} +export interface Interface { + readonly directory: AbsolutePath + readonly workspaceID?: string + readonly project: { + readonly id: Project.ID + readonly directory: AbsolutePath + } + readonly vcs?: Project.Vcs +} + +export class Service extends Context.Service()("@opencode/Location") {} + +export const layer = (ref: Ref) => + Layer.effect( + Service, + Effect.gen(function* () { + const project = yield* Project.Service + const resolved = yield* project.resolve(ref.directory) + return Service.of({ + directory: ref.directory, + workspaceID: ref.workspaceID, + project: { id: resolved.id, directory: resolved.directory }, + vcs: resolved.vcs, + }) + }), + ) + +export const defaultLayer = (ref: Ref) => layer(ref).pipe(Layer.provide(Project.defaultLayer)) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 77b8c60ebe7..8cf02ddfe05 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -36,7 +36,7 @@ export const Cost = Schema.Struct({ export const Ref = Schema.Struct({ id: ID, providerID: ProviderV2.ID, - variant: VariantID, + variant: VariantID.pipe(Schema.optional), }) export type Ref = typeof Ref.Type diff --git a/packages/core/src/models.ts b/packages/core/src/models-dev.ts similarity index 92% rename from packages/core/src/models.ts rename to packages/core/src/models-dev.ts index f69ecc6d299..fec7e001c28 100644 --- a/packages/core/src/models.ts +++ b/packages/core/src/models-dev.ts @@ -8,6 +8,7 @@ import { Hash } from "./util/hash" import { AppFileSystem } from "./filesystem" import { InstallationChannel, InstallationVersion } from "./installation/version" import * as ModelsRefresh from "./kilocode/models-refresh" // kilocode_change +import { EventV2 } from "./event" export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type @@ -114,6 +115,15 @@ export const Provider = Schema.Struct({ export type Provider = Schema.Schema.Type +export const Event = { + Refreshed: EventV2.define({ + type: "models-dev.refreshed", + schema: {}, + }), +} + +declare const KILO_MODELS_DEV: Record | undefined + export interface Interface { readonly get: () => Effect.Effect> readonly refresh: (force?: boolean) => Effect.Effect @@ -121,12 +131,11 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ModelsDev") {} -type Requirements = AppFileSystem.Service | HttpClient.HttpClient - -export const layer: Layer.Layer = Layer.effect( +export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service + const events = yield* EventV2.Service const http = HttpClient.filterStatusOk( (yield* HttpClient.HttpClient).pipe( HttpClient.retryTransient({ @@ -166,12 +175,7 @@ export const layer: Layer.Layer = Layer.effect( Effect.map((v) => v as Record | undefined), ) - // Bundled at build time; absent in dev — `tryPromise` covers both. - const loadSnapshot = Effect.tryPromise({ - // @ts-ignore — generated at build time, may not exist in dev - try: () => import("./models-snapshot.js").then((m) => m.snapshot as Record | undefined), - catch: () => undefined, - }).pipe(Effect.catch(() => Effect.succeed(undefined))) + const loadSnapshot = Effect.sync(() => (typeof KILO_MODELS_DEV === "undefined" ? undefined : KILO_MODELS_DEV)) const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { const text = yield* fetchApi() @@ -210,6 +214,7 @@ export const layer: Layer.Layer = Layer.effect( yield* fetchAndWrite() yield* invalidate yield* ModelsRefresh.notify() // kilocode_change + yield* events.publish(Event.Refreshed, {}) }), ).pipe( Effect.tapCause((cause) => @@ -228,9 +233,10 @@ export const layer: Layer.Layer = Layer.effect( }), ) -export const defaultLayer: Layer.Layer = layer.pipe( +export const defaultLayer = layer.pipe( Layer.provide(FetchHttpClient.layer), Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(EventV2.defaultLayer), ) -export * as ModelsDev from "./models" +export * as ModelsDev from "./models-dev" diff --git a/packages/core/src/models-snapshot.d.ts b/packages/core/src/models-snapshot.d.ts deleted file mode 100644 index 839eba6b7d5..00000000000 --- a/packages/core/src/models-snapshot.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Auto-generated by build.ts - do not edit -export declare const snapshot: Record diff --git a/packages/core/src/models-snapshot.js b/packages/core/src/models-snapshot.js deleted file mode 100644 index c582a75bdfd..00000000000 --- a/packages/core/src/models-snapshot.js +++ /dev/null @@ -1,71726 +0,0 @@ -// @ts-nocheck -// Auto-generated by build.ts - do not edit -export const snapshot = { - "302ai": { - id: "302ai", - env: ["302AI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.302.ai/v1", - name: "302.AI", - doc: "https://doc.302.ai", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 2.86 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4.1": { - id: "grok-4.1", - name: "grok-4.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10 }, - limit: { context: 200000, output: 64000 }, - }, - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-26", - last_updated: "2025-10-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 1000000, output: 128000 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "grok-4-1-fast-reasoning", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "gemini-2.5-flash-nothink": { - id: "gemini-2.5-flash-nothink", - name: "gemini-2.5-flash-nothink", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-24", - last_updated: "2025-06-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "grok-4.20-multi-agent-beta-0309": { - id: "grok-4.20-multi-agent-beta-0309", - name: "grok-4.20-multi-agent-beta-0309", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 2000000, output: 30000 }, - }, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "kimi-k2-0905-preview", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.632, output: 2.53 }, - limit: { context: 262144, output: 262144 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "claude-haiku-4-5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "claude-opus-4-5-20251101", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "gemini-2.5-flash-lite-preview-09-2025", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "qwen3-235b-a22b-instruct-2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1.143 }, - limit: { context: 128000, output: 65536 }, - }, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 3.2 }, - limit: { context: 200000, output: 131072 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "mistral-large-2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 3.3 }, - limit: { context: 128000, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "glm-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 204800, output: 131072 }, - }, - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "claude-3-5-haiku-20241022", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4 }, - limit: { context: 200000, output: 8192 }, - }, - "doubao-seed-1-8-251215": { - id: "doubao-seed-1-8-251215", - name: "doubao-seed-1-8-251215", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.114, output: 0.286 }, - limit: { context: 224000, output: 64000 }, - }, - "chatgpt-4o-latest": { - id: "chatgpt-4o-latest", - name: "chatgpt-4o-latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2024-08-08", - last_updated: "2024-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "glm-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.6 }, - limit: { context: 204800, output: 131072 }, - }, - "deepseek-chat": { - id: "deepseek-chat", - name: "Deepseek-Chat", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-v3.2-thinking": { - id: "deepseek-v3.2-thinking", - name: "DeepSeek-V3.2-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 128000 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "claude-sonnet-4-6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-18", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 1000000, output: 64000 }, - }, - "gpt-5-thinking": { - id: "gpt-5-thinking", - name: "gpt-5-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "glm-4.7-flashx", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-20", - last_updated: "2026-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0715, output: 0.429 }, - limit: { context: 200000, output: 131072 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen-Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 1.2 }, - limit: { context: 1000000, output: 32768 }, - }, - "grok-4.20-beta-0309-non-reasoning": { - id: "grok-4.20-beta-0309-non-reasoning", - name: "grok-4.20-beta-0309-non-reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "claude-opus-4-7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-01-31", - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "gemini-3-pro-preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1000000, output: 64000 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen3-max-2025-09-23": { - id: "qwen3-max-2025-09-23", - name: "qwen3-max-2025-09-23", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.43 }, - limit: { context: 258048, output: 65536 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "claude-sonnet-4-5-20250929", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen-Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.022, output: 0.22 }, - limit: { context: 1000000, output: 32768 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1000000, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "grok-4-1-fast-non-reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "claude-3-5-haiku-latest", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-5-20251101-thinking": { - id: "claude-opus-4-5-20251101-thinking", - name: "claude-opus-4-5-20251101-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "gpt-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "gpt-5.4-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-3-pro-image-preview": { - id: "gemini-3-pro-image-preview", - name: "gemini-3-pro-image-preview", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 120 }, - limit: { context: 32768, output: 64000 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "glm-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-10", - last_updated: "2026-04-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.5 }, - limit: { context: 200000, output: 131072 }, - }, - "qwen-max-latest": { - id: "qwen-max-latest", - name: "Qwen-Max-Latest", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.343, output: 1.372 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "gpt-5.4-nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "gemini-2.5-flash-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-10-08", - last_updated: "2025-10-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 30 }, - limit: { context: 32768, output: 32768 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 131072, output: 98304 }, - }, - "gpt-5.4-mini-2026-03-17": { - id: "gpt-5.4-mini-2026-03-17", - name: "gpt-5.4-mini-2026-03-17", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "gpt-5.2-chat-latest", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16384 }, - }, - "doubao-seed-1-6-vision-250815": { - id: "doubao-seed-1-6-vision-250815", - name: "doubao-seed-1-6-vision-250815", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.114, output: 1.143 }, - limit: { context: 256000, output: 32000 }, - }, - "gemini-3.1-flash-image-preview": { - id: "gemini-3.1-flash-image-preview", - name: "gemini-3.1-flash-image-preview", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-27", - last_updated: "2026-02-27", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.5, output: 60 }, - limit: { context: 131072, output: 32768 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4.8 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.5-x": { - id: "glm-4.5-x", - name: "glm-4.5-x", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.143, output: 2.29 }, - limit: { context: 128000, output: 16384 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 1000000, output: 131072 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "gpt-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "kimi-k2-thinking-turbo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.265, output: 9.119 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "Deepseek-Reasoner", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "grok-4-fast-reasoning", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-opus-4-1-20250805-thinking": { - id: "claude-opus-4-1-20250805-thinking", - name: "claude-opus-4-1-20250805-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-27", - last_updated: "2025-05-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "glm-4.5-air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1143, output: 0.286 }, - limit: { context: 131072, output: 98304 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "gpt-5.4-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 0, cache_write: 0, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "glm-5-turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 3.2 }, - limit: { context: 200000, output: 131072 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3-30B-A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 1.08 }, - limit: { context: 128000, output: 8192 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "claude-opus-4-5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.86 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "glm-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.286, output: 1.142 }, - limit: { context: 204800, output: 131072 }, - }, - "claude-opus-4-6-thinking": { - id: "claude-opus-4-6-thinking", - name: "claude-opus-4-6-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-06", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "gemini-2.5-flash-preview-09-2025", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "claude-sonnet-4-6-thinking": { - id: "claude-sonnet-4-6-thinking", - name: "claude-sonnet-4-6-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-18", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 1000000, output: 64000 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.145, output: 0.43 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "claude-opus-4-1-20250805", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "gpt-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - cache_write: 0, - context_over_200k: { input: 5, output: 22.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "gpt-5.1-chat-latest", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "claude-haiku-4-5-20251001", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, output: 64000 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax-M1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.132, output: 1.254 }, - limit: { context: 1000000, output: 128000 }, - }, - "gpt-5.4-nano-2026-03-17": { - id: "gpt-5.4-nano-2026-03-17", - name: "gpt-5.4-nano-2026-03-17", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-19", - last_updated: "2026-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "claude-sonnet-4-20250514", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "qwen3-coder-480b-a35b-instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.43 }, - limit: { context: 262144, output: 65536 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "claude-opus-4-6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-06", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - "doubao-seed-code-preview-251028": { - id: "doubao-seed-code-preview-251028", - name: "doubao-seed-code-preview-251028", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-11", - last_updated: "2025-11-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.14 }, - limit: { context: 256000, output: 32000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "gpt-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "deepseek-v3.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.43 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "gpt-5-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-08", - last_updated: "2025-10-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 272000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "gpt-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "claude-sonnet-4-5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "gpt-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-4.20-beta-0309-reasoning": { - id: "grok-4.20-beta-0309-reasoning", - name: "grok-4.20-beta-0309-reasoning", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "claude-opus-4-20250514", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "glm-for-coding": { - id: "glm-for-coding", - name: "glm-for-coding", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.343 }, - limit: { context: 200000, output: 131072 }, - }, - "claude-sonnet-4-5-20250929-thinking": { - id: "claude-sonnet-4-5-20250929-thinking", - name: "claude-sonnet-4-5-20250929-thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "glm-4.5-airx": { - id: "glm-4.5-airx", - name: "glm-4.5-airx", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.572, output: 1.714 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "gpt-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "kimi-k2-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.575, output: 2.3 }, - limit: { context: 262144, output: 262144 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "gemini-2.0-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-11", - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 2000000, output: 8192 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "gpt-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1047576, output: 32768 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "grok-4-fast-non-reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 30000 }, - }, - "doubao-seed-1-6-thinking-250715": { - id: "doubao-seed-1-6-thinking-250715", - name: "doubao-seed-1-6-thinking-250715", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.121, output: 1.21 }, - limit: { context: 256000, output: 16000 }, - }, - "ministral-14b-2512": { - id: "ministral-14b-2512", - name: "ministral-14b-2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 0.33 }, - limit: { context: 128000, output: 128000 }, - }, - }, - }, - alibaba: { - id: "alibaba", - env: ["DASHSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - name: "Alibaba", - doc: "https://www.alibabacloud.com/help/en/model-studio/models", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen3.5-122b-a10b": { - id: "qwen3.5-122b-a10b", - name: "Qwen3.5 122B-A10B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-23", - last_updated: "2026-02-23", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 3.2 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen3.6-27b": { - id: "qwen3.6-27b", - name: "Qwen3.6 27B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.5-27b": { - id: "qwen3.5-27b", - name: "Qwen3.5 27B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-23", - last_updated: "2026-02-23", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.4 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-vl-ocr": { - id: "qwen-vl-ocr", - name: "Qwen-VL OCR", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-28", - last_updated: "2025-04-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 34096, output: 4096 }, - }, - "qwen-omni-turbo-realtime": { - id: "qwen-omni-turbo-realtime", - name: "Qwen-Omni Turbo Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.27, output: 1.07, input_audio: 4.44, output_audio: 8.89 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3-8b": { - id: "qwen3-8b", - name: "Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.7, reasoning: 2.1 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen-VL Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-08-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.63 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-livetranslate-flash-realtime": { - id: "qwen3-livetranslate-flash-realtime", - name: "Qwen3-LiveTranslate Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 10, output: 10, input_audio: 10, output_audio: 38 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.2, reasoning: 4 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen3.6-35b-a3b": { - id: "qwen3.6-35b-a3b", - name: "Qwen3.6 35B-A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.248, output: 1.485 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen-Omni Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-19", - last_updated: "2025-03-26", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.07, output: 0.27, input_audio: 4.44, output_audio: 8.89 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen2-5-vl-7b-instruct": { - id: "qwen2-5-vl-7b-instruct", - name: "Qwen2.5-VL 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.05 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.276, output: 1.651, cache_read: 0.028, cache_write: 0.344 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-omni-flash": { - id: "qwen3-omni-flash", - name: "Qwen3-Omni Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.43, output: 1.66, input_audio: 3.81, output_audio: 15.11 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen2-5-72b-instruct": { - id: "qwen2-5-72b-instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 5.6 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3-VL 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-asr-flash": { - id: "qwen3-asr-flash", - name: "Qwen3-ASR Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.035 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next 80B-A3B (Thinking)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-mt-plus": { - id: "qwen-mt-plus", - name: "Qwen-MT Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.46, output: 7.37 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen-VL Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen2-5-7b-instruct": { - id: "qwen2-5-7b-instruct", - name: "Qwen2.5 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.175, output: 0.7 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-14b-instruct": { - id: "qwen2-5-14b-instruct", - name: "Qwen2.5 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-32b-instruct": { - id: "qwen2-5-32b-instruct", - name: "Qwen2.5 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next 80B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-plus-character-ja": { - id: "qwen-plus-character-ja", - name: "Qwen Plus Character (Japanese)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.4 }, - limit: { context: 8192, output: 512 }, - }, - "qwen3-omni-flash-realtime": { - id: "qwen3-omni-flash-realtime", - name: "Qwen3-Omni Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.52, output: 1.99, input_audio: 4.57, output_audio: 18.13 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen3-vl-30b-a3b": { - id: "qwen3-vl-30b-a3b", - name: "Qwen3-VL 30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8, reasoning: 2.4 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.6, reasoning: 4.8 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3-Coder 480B-A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.25 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11-01", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.2, reasoning: 0.5 }, - limit: { context: 1000000, output: 16384 }, - }, - "qwen-mt-turbo": { - id: "qwen-mt-turbo", - name: "Qwen-MT Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.49 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen3.6-max-preview": { - id: "qwen3.6-max-preview", - name: "Qwen3.6 Max Preview", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.3, output: 7.8, cache_read: 0.13, cache_write: 1.625 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen2-5-omni-7b": { - id: "qwen2-5-omni-7b", - name: "Qwen2.5-Omni 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4, input_audio: 6.76 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, reasoning: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5-VL 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.8, output: 8.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qvq-max": { - id: "qvq-max", - name: "QVQ Max", - family: "qvq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4.8 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-14b": { - id: "qwen3-14b", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.4, reasoning: 4.2 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.5-35b-a3b": { - id: "qwen3.5-35b-a3b", - name: "Qwen3.5 35B-A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-23", - last_updated: "2026-02-23", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - scaleway: { - id: "scaleway", - env: ["SCALEWAY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.scaleway.ai/v1", - name: "Scaleway", - doc: "https://www.scaleway.com/en/docs/generative-apis/", - models: { - "qwen3-embedding-8b": { - id: "qwen3-embedding-8b", - name: "Qwen3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-25-11", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 32768, output: 4096 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 2.25 }, - limit: { context: 260000, output: 16384 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 100000, output: 16384 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 256000, output: 16384 }, - }, - "devstral-2-123b-instruct-2512": { - id: "devstral-2-123b-instruct-2512", - name: "Devstral 2 123B Instruct (2512)", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-07", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, output: 16384 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 32000, output: 8196 }, - }, - "pixtral-12b-2409": { - id: "pixtral-12b-2409", - name: "Pixtral 12B 2409", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-25", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper Large v3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2026-03-17", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.003, output: 0 }, - limit: { context: 0, output: 8192 }, - }, - "voxtral-small-24b-2507": { - id: "voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - family: "voxtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-17", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 32000, output: 16384 }, - }, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma-3-27B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.5 }, - limit: { context: 40000, output: 8192 }, - }, - "bge-multilingual-gemma2": { - id: "bge-multilingual-gemma2", - name: "BGE Multilingual Gemma2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-07-26", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-small-3.2-24b-instruct-2506": { - id: "mistral-small-3.2-24b-instruct-2506", - name: "Mistral Small 3.2 24B Instruct (2506)", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-20", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-nemo-instruct-2407": { - id: "mistral-nemo-instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral-nemo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-25", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 16384 }, - }, - }, - }, - "nano-gpt": { - id: "nano-gpt", - env: ["NANO_GPT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://nano-gpt.com/api/v1", - name: "NanoGPT", - doc: "https://docs.nano-gpt.com", - models: { - "glm-4-flash": { - id: "glm-4-flash", - name: "GLM-4 Flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Meta-Llama-3-1-8B-Instruct-FP8": { - id: "Meta-Llama-3-1-8B-Instruct-FP8", - name: "Llama 3.1 8B (decentralized)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "claude-opus-4-thinking:32000": { - id: "claude-opus-4-thinking:32000", - name: "Claude 4 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 0506", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "grok-3-mini-fast-beta": { - id: "grok-3-mini-fast-beta", - name: "Grok 3 Mini Fast Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax M2", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-10-25", - last_updated: "2025-10-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.53 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "command-a-reasoning-08-2025": { - id: "command-a-reasoning-08-2025", - name: "Cohere Command A (08/2025)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-22", - last_updated: "2025-08-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - brave: { - id: "brave", - name: "Brave (Answers)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "exa-research": { - id: "exa-research", - name: "Exa (Research)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "Llama-3.3-70B-Nova": { - id: "Llama-3.3-70B-Nova", - name: "Llama 3.3 70B Nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-exp-1206": { - id: "gemini-exp-1206", - name: "Gemini 2.0 Pro 1206", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.258, output: 4.998 }, - limit: { context: 2097152, input: 2097152, output: 8192 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude 4.5 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "auto-model-basic": { - id: "auto-model-basic", - name: "Auto model (Basic)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "jamba-mini": { - id: "jamba-mini", - name: "Jamba Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview (09/2025)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "yi-large": { - id: "yi-large", - name: "Yi Large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.196, output: 3.196 }, - limit: { context: 32000, input: 32000, output: 4096 }, - }, - "auto-model-premium": { - id: "auto-model-premium", - name: "Auto model (Premium)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "azure-gpt-4o": { - id: "azure-gpt-4o", - name: "Azure gpt-4o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek Chat 0324", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "doubao-seed-1-8-251215": { - id: "doubao-seed-1-8-251215", - name: "Doubao Seed 1.8", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.612, output: 6.12 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "doubao-seed-1-6-250615": { - id: "doubao-seed-1-6-250615", - name: "Doubao Seed 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.204, output: 0.51 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "ernie-x1.1-preview": { - id: "ernie-x1.1-preview", - name: "ERNIE X1.1", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "ernie-5.0-thinking-preview": { - id: "ernie-5.0-thinking-preview", - name: "Ernie 5.0 Thinking Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "glm-4-air-0111": { - id: "glm-4-air-0111", - name: "GLM 4 Air 0111", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-11", - last_updated: "2025-01-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 0.1394 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - fastgpt: { - id: "fastgpt", - name: "Web Answer", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-08-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.5, output: 7.5 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "doubao-seed-1-6-thinking-250615": { - id: "doubao-seed-1-6-thinking-250615", - name: "Doubao Seed 1.6 Thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.204, output: 2.04 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "gemini-2.0-flash-001": { - id: "gemini-2.0-flash-001", - name: "Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "claude-opus-4-1-thinking:32000": { - id: "claude-opus-4-1-thinking:32000", - name: "Claude 4.1 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-RAWMAW": { - id: "Llama-3.3-70B-RAWMAW", - name: "Llama 3.3 70B RAWMAW", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Steam": { - id: "GLM-4.5-Air-Derestricted-Steam", - name: "GLM 4.5 Air Derestricted Steam", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 220600, input: 220600, output: 65536 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude 3.5 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "yi-medium-200k": { - id: "yi-medium-200k", - name: "Yi Medium 200k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-03-01", - last_updated: "2024-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 2.499 }, - limit: { context: 200000, input: 200000, output: 4096 }, - }, - "Gemma-3-27B-ArliAI-RPMax-v3": { - id: "Gemma-3-27B-ArliAI-RPMax-v3", - name: "Gemma 3 27B RPMax v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "phi-4-mini-instruct": { - id: "phi-4-mini-instruct", - name: "Phi 4 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3+(3v3.3)-70B-TenyxChat-DaybreakStorywriter": { - id: "Llama-3.3+(3v3.3)-70B-TenyxChat-DaybreakStorywriter", - name: "Llama 3.3+ 70B TenyxChat DaybreakStorywriter", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "ernie-x1-32k": { - id: "ernie-x1-32k", - name: "Ernie X1 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "deepseek-chat": { - id: "deepseek-chat", - name: "DeepSeek V3/Deepseek Chat", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "glm-z1-air": { - id: "glm-z1-air", - name: "GLM Z1 Air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "claude-3-7-sonnet-thinking:128000": { - id: "claude-3-7-sonnet-thinking:128000", - name: "Claude 3.7 Sonnet Thinking (128K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "glm-4-air": { - id: "glm-4-air", - name: "GLM-4 Air", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Llama-3.3-70B-MiraiFanfare": { - id: "Llama-3.3-70B-MiraiFanfare", - name: "Llama 3.3 70b Mirai Fanfare", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.493, output: 0.493 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-2.0-flash-thinking-exp-01-21": { - id: "gemini-2.0-flash-thinking-exp-01-21", - name: "Gemini 2.0 Flash Thinking 0121", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-21", - last_updated: "2025-01-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 1.003 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "Magistral-Small-2506": { - id: "Magistral-Small-2506", - name: "Magistral Small 2506", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.4 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "doubao-1.5-pro-32k": { - id: "doubao-1.5-pro-32k", - name: "Doubao 1.5 Pro 32k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-22", - last_updated: "2025-01-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1343, output: 0.3349 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "venice-uncensored:web": { - id: "venice-uncensored:web", - name: "Venice Uncensored Web", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-01", - last_updated: "2024-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 80000, input: 80000, output: 16384 }, - }, - "glm-4": { - id: "glm-4", - name: "GLM-4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-01-16", - last_updated: "2024-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 14.994 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen 2.5 Max", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-04-03", - last_updated: "2024-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5997, output: 6.392 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "qwen3-vl-235b-a22b-instruct-original": { - id: "qwen3-vl-235b-a22b-instruct-original", - name: "Qwen3 VL 235B A22B Instruct Original", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "jamba-large-1.6": { - id: "jamba-large-1.6", - name: "Jamba Large 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3995, output: 1.2002 }, - limit: { context: 995904, input: 995904, output: 32768 }, - }, - "qwen25-vl-72b-instruct": { - id: "qwen25-vl-72b-instruct", - name: "Qwen25 VL 72b", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.69989, output: 0.69989 }, - limit: { context: 32000, input: 32000, output: 32768 }, - }, - "claude-sonnet-4-thinking:64000": { - id: "claude-sonnet-4-thinking:64000", - name: "Claude 4 Sonnet Thinking (64K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3+(3.1v3.3)-70B-New-Dawn-v1.1": { - id: "Llama-3.3+(3.1v3.3)-70B-New-Dawn-v1.1", - name: "Llama 3.3+ 70B New Dawn v1.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Iceblink-ReExtract", - name: "GLM 4.5 Air Derestricted Iceblink ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 98304 }, - }, - "universal-summarizer": { - id: "universal-summarizer", - name: "Universal Summarizer", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-05-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 30 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-sonnet-4-thinking:32768": { - id: "claude-sonnet-4-thinking:32768", - name: "Claude 4 Sonnet Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "sarvan-medium": { - id: "sarvan-medium", - name: "Sarvam Medium", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "claude-3-7-sonnet-thinking:8192": { - id: "claude-3-7-sonnet-thinking:8192", - name: "Claude 3.7 Sonnet Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash 0520", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048000, input: 1048000, output: 65536 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-v2-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Iceblink-v2-ReExtract", - name: "GLM 4.5 Air Derestricted Iceblink v2 ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 65536 }, - }, - "Llama-3.3-70B-Fallen-v1": { - id: "Llama-3.3-70B-Fallen-v1", - name: "Llama 3.3 70B Fallen v1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen3-vl-235b-a22b-thinking": { - id: "qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 6 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-3-7-sonnet-thinking:32768": { - id: "claude-3-7-sonnet-thinking:32768", - name: "Claude 3.7 Sonnet Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "claude-3-7-sonnet-thinking:1024": { - id: "claude-3-7-sonnet-thinking:1024", - name: "Claude 3.7 Sonnet Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "Llama-3.3-70B-Vulpecula-R1": { - id: "Llama-3.3-70B-Vulpecula-R1", - name: "Llama 3.3 70B Vulpecula R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-sonnet-4-thinking:8192": { - id: "claude-sonnet-4-thinking:8192", - name: "Claude 4 Sonnet Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-Ignition-v0.1": { - id: "Llama-3.3-70B-Ignition-v0.1", - name: "Llama 3.3 70B Ignition v0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "glm-4-plus-0111": { - id: "glm-4-plus-0111", - name: "GLM 4 Plus 0111", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "KAT-Coder-Air-V1": { - id: "KAT-Coder-Air-V1", - name: "KAT Coder Air V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "deepseek-r1-sambanova": { - id: "deepseek-r1-sambanova", - name: "DeepSeek R1 Fast", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 6.987 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek R1", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "doubao-1-5-thinking-pro-250415": { - id: "doubao-1-5-thinking-pro-250415", - name: "Doubao 1.5 Thinking Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Perplexity Pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "Gemma-3-27B-it-Abliterated": { - id: "Gemma-3-27B-it-Abliterated", - name: "Gemma 3 27B IT Abliterated", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.42, output: 0.42 }, - limit: { context: 32768, input: 32768, output: 96000 }, - }, - "deepseek-chat-cheaper": { - id: "deepseek-chat-cheaper", - name: "DeepSeek V3/Chat Cheaper", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "gemini-2.0-pro-exp-02-05": { - id: "gemini-2.0-pro-exp-02-05", - name: "Gemini 2.0 Pro 0205", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.956 }, - limit: { context: 2097152, input: 2097152, output: 8192 }, - }, - "azure-gpt-4o-mini": { - id: "azure-gpt-4o-mini", - name: "Azure gpt-4o-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1496, output: 0.595 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3-70B-MS-Nevoria": { - id: "Llama-3.3-70B-MS-Nevoria", - name: "Llama 3.3 70B MS Nevoria", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-thinking": { - id: "claude-opus-4-thinking", - name: "Claude 4 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Sapphira-0.1": { - id: "Llama-3.3-70B-Sapphira-0.1", - name: "Llama 3.3 70B Sapphira 0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "doubao-seed-code-preview-latest": { - id: "doubao-seed-code-preview-latest", - name: "Doubao Seed Code Preview", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "qwen-3.6-plus": { - id: "qwen-3.6-plus", - name: "Qwen 3.6 Plus", - family: "qwen3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 2.7 }, - limit: { context: 991800, output: 65536 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v1.4": { - id: "Llama-3.3-70B-ArliAI-RPMax-v1.4", - name: "Llama 3.3 70B RPMax v1.4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "mistral-small-31-24b-instruct": { - id: "mistral-small-31-24b-instruct", - name: "Mistral Small 31 24b Instruct", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "glm-4.1v-thinking-flashx": { - id: "glm-4.1v-thinking-flashx", - name: "GLM 4.1V Thinking FlashX", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "hunyuan-t1-latest": { - id: "hunyuan-t1-latest", - name: "Hunyuan T1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-22", - last_updated: "2025-03-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "doubao-1-5-thinking-vision-pro-250428": { - id: "doubao-1-5-thinking-vision-pro-250428", - name: "Doubao 1.5 Thinking Vision Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-15", - last_updated: "2025-05-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 1.43 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "asi1-mini": { - id: "asi1-mini", - name: "ASI1 Mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "ernie-5.0-thinking-latest": { - id: "ernie-5.0-thinking-latest", - name: "Ernie 5.0 Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Llama-3.3-70B-Incandescent-Malevolence": { - id: "Llama-3.3-70B-Incandescent-Malevolence", - name: "Llama 3.3 70B Incandescent Malevolence", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Damascus-R1": { - id: "Llama-3.3-70B-Damascus-R1", - name: "Damascus R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Gemma-3-27B-Nidum-Uncensored": { - id: "Gemma-3-27B-Nidum-Uncensored", - name: "Gemma 3 27B Nidum Uncensored", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 96000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025-thinking": { - id: "gemini-2.5-flash-lite-preview-09-2025-thinking", - name: "Gemini 2.5 Flash Lite Preview (09/2025) – Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "doubao-seed-2-0-pro-260215": { - id: "doubao-seed-2-0-pro-260215", - name: "Doubao Seed 2.0 Pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.782, output: 3.876 }, - limit: { context: 256000, input: 256000, output: 128000 }, - }, - "gemini-3-pro-image-preview": { - id: "gemini-3-pro-image-preview", - name: "Gemini 3 Pro Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Gemma-3-27B-CardProjector-v4": { - id: "Gemma-3-27B-CardProjector-v4", - name: "Gemma 3 27B CardProjector v4", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "jamba-mini-1.7": { - id: "jamba-mini-1.7", - name: "Jamba Mini 1.7", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "Llama-3.3-70B-Forgotten-Safeword-3.6": { - id: "Llama-3.3-70B-Forgotten-Safeword-3.6", - name: "Llama 3.3 70B Forgotten Safeword 3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "doubao-1-5-thinking-pro-vision-250415": { - id: "doubao-1-5-thinking-pro-vision-250415", - name: "Doubao 1.5 Thinking Pro Vision", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 0605", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "gemini-2.0-pro-reasoner": { - id: "gemini-2.0-pro-reasoner", - name: "Gemini 2.0 Pro Reasoner", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.292, output: 4.998 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "doubao-seed-2-0-lite-260215": { - id: "doubao-seed-2-0-lite-260215", - name: "Doubao Seed 2.0 Lite", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1462, output: 0.8738 }, - limit: { context: 256000, input: 256000, output: 32000 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Deep Research", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-25", - last_updated: "2025-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.4, output: 13.6 }, - limit: { context: 60000, input: 60000, output: 128000 }, - }, - "Gemma-3-27B-it": { - id: "Gemma-3-27B-it", - name: "Gemma 3 27B IT", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-GeneticLemonade-Unleashed-v3": { - id: "Llama-3.3-70B-GeneticLemonade-Unleashed-v3", - name: "Llama 3.3 70B GeneticLemonade Unleashed v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Gemma-3-27B-Glitter": { - id: "Gemma-3-27B-Glitter", - name: "Gemma 3 27B Glitter", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.1": { - id: "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.1", - name: "Llama 3.3 70B Omega Directive Unslop v2.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "gemini-2.5-flash-preview-09-2025-thinking": { - id: "gemini-2.5-flash-preview-09-2025-thinking", - name: "Gemini 2.5 Flash Preview (09/2025) – Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - deepclaude: { - id: "deepclaude", - name: "DeepClaude", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "ernie-4.5-8k-preview": { - id: "ernie-4.5-8k-preview", - name: "Ernie 4.5 8k Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 2.6 }, - limit: { context: 8000, input: 8000, output: 16384 }, - }, - "doubao-seed-2-0-mini-260215": { - id: "doubao-seed-2-0-mini-260215", - name: "Doubao Seed 2.0 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0493, output: 0.4845 }, - limit: { context: 256000, input: 256000, output: 32000 }, - }, - "gemini-3-pro-preview-thinking": { - id: "gemini-3-pro-preview-thinking", - name: "Gemini 3 Pro Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-GeneticLemonade-Opus": { - id: "Llama-3.3-70B-GeneticLemonade-Opus", - name: "Llama 3.3 70B GeneticLemonade Opus", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "v0-1.5-lg": { - id: "v0-1.5-lg", - name: "v0 1.5 LG", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "ernie-4.5-turbo-128k": { - id: "ernie-4.5-turbo-128k", - name: "Ernie 4.5 Turbo 128k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.132, output: 0.55 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "KAT-Coder-Pro-V1": { - id: "KAT-Coder-Pro-V1", - name: "KAT Coder Pro V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude 3.5 Sonnet Old", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 8192 }, - }, - "claude-opus-4-1-thinking:8192": { - id: "claude-opus-4-1-thinking:8192", - name: "Claude 4.1 Opus Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.0-flash-exp-image-generation": { - id: "gemini-2.0-flash-exp-image-generation", - name: "Gemini Text + Image", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "Llama-3.3-70B-Magnum-v4-SE": { - id: "Llama-3.3-70B-Magnum-v4-SE", - name: "Llama 3.3 70B Magnum v4 SE", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "glm-zero-preview": { - id: "glm-zero-preview", - name: "GLM Zero Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.802, output: 1.802 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "study_gpt-chatgpt-4o-latest": { - id: "study_gpt-chatgpt-4o-latest", - name: "Study Mode", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16384 }, - }, - "glm-4-airx": { - id: "glm-4-airx", - name: "GLM-4 AirX", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "step-2-mini": { - id: "step-2-mini", - name: "Step-2 Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-05", - last_updated: "2024-07-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.408 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "gemini-2.5-flash-preview-04-17:thinking": { - id: "gemini-2.5-flash-preview-04-17:thinking", - name: "Gemini 2.5 Flash Preview Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 3.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "Llama-3.3-70B-Mokume-Gane-R1": { - id: "Llama-3.3-70B-Mokume-Gane-R1", - name: "Llama 3.3 70B Mokume Gane R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 64000, input: 64000, output: 65536 }, - }, - "glm-z1-airx": { - id: "glm-z1-airx", - name: "GLM Z1 AirX", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "jamba-mini-1.6": { - id: "jamba-mini-1.6", - name: "Jamba Mini 1.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.408 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "claude-opus-4-1-thinking": { - id: "claude-opus-4-1-thinking", - name: "Claude 4.1 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "grok-3-beta": { - id: "grok-3-beta", - name: "Grok 3 Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "Llama-3.3-70B-Legion-V2.1": { - id: "Llama-3.3-70B-Legion-V2.1", - name: "Llama 3.3 70B Legion V2.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - sonar: { - id: "sonar", - name: "Perplexity Simple", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.003, output: 1.003 }, - limit: { context: 127000, input: 127000, output: 128000 }, - }, - "z-image-turbo": { - id: "z-image-turbo", - name: "Z Image Turbo", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-27", - last_updated: "2025-11-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink-v2": { - id: "GLM-4.5-Air-Derestricted-Iceblink-v2", - name: "GLM 4.5 Air Derestricted Iceblink v2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 158600, input: 158600, output: 65536 }, - }, - "jamba-large": { - id: "jamba-large", - name: "Jamba Large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "claude-3-7-sonnet-reasoner": { - id: "claude-3-7-sonnet-reasoner", - name: "Claude 3.7 Sonnet Reasoner", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-29", - last_updated: "2025-03-29", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "ernie-4.5-turbo-vl-32k": { - id: "ernie-4.5-turbo-vl-32k", - name: "Ernie 4.5 Turbo VL 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.495, output: 1.43 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "Mistral-Nemo-12B-Instruct-2407": { - id: "Mistral-Nemo-12B-Instruct-2407", - name: "Mistral Nemo 12B Instruct 2407", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "doubao-seed-1-6-flash-250615": { - id: "doubao-seed-1-6-flash-250615", - name: "Doubao Seed 1.6 Flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0374, output: 0.374 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "qwq-32b": { - id: "qwq-32b", - name: "Qwen: QwQ 32B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25599999, output: 0.30499999 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "Llama-3.3-70B-Strawberrylemonade-v1.2": { - id: "Llama-3.3-70B-Strawberrylemonade-v1.2", - name: "Llama 3.3 70B StrawberryLemonade v1.2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "ernie-x1-turbo-32k": { - id: "ernie-x1-turbo-32k", - name: "Ernie X1 Turbo 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.165, output: 0.66 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "deepseek-math-v2": { - id: "deepseek-math-v2", - name: "DeepSeek Math V2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "Llama-3.3-70B-Electranova-v1.0": { - id: "Llama-3.3-70B-Electranova-v1.0", - name: "Llama 3.3 70B Electranova v1.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v2": { - id: "Llama-3.3-70B-ArliAI-RPMax-v2", - name: "Llama 3.3 70B ArliAI RPMax v2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "qwen-image": { - id: "qwen-image", - name: "Qwen Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "Llama-3.3-70B-Cu-Mai-R1": { - id: "Llama-3.3-70B-Cu-Mai-R1", - name: "Llama 3.3 70B Cu Mai R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "GLM-4.5-Air-Derestricted-Iceblink": { - id: "GLM-4.5-Air-Derestricted-Iceblink", - name: "GLM 4.5 Air Derestricted Iceblink", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 98304 }, - }, - "Llama-3.3-70B-Bigger-Body": { - id: "Llama-3.3-70B-Bigger-Body", - name: "Llama 3.3 70B Bigger Body", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3+(3.1v3.3)-70B-Hanami-x1": { - id: "Llama-3.3+(3.1v3.3)-70B-Hanami-x1", - name: "Llama 3.3+ 70B Hanami x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "hunyuan-turbos-20250226": { - id: "hunyuan-turbos-20250226", - name: "Hunyuan Turbo S", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.187, output: 0.374 }, - limit: { context: 24000, input: 24000, output: 8192 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview (09/2025)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "GLM-4.6-Derestricted-v5": { - id: "GLM-4.6-Derestricted-v5", - name: "GLM 4.6 Derestricted v5", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "glm-4-plus": { - id: "glm-4-plus", - name: "GLM-4 Plus", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.497, output: 7.497 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Gemma-3-27B-Big-Tiger-v3": { - id: "Gemma-3-27B-Big-Tiger-v3", - name: "Gemma 3 27B Big Tiger v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "brave-research": { - id: "brave-research", - name: "Brave (Research)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - hidream: { - id: "hidream", - name: "Hidream", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max 2026-01-23", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2002, output: 6.001 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude 4.1 Opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax M1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 1.3328 }, - limit: { context: 1000000, input: 1000000, output: 131072 }, - }, - "gemini-2.5-flash-nothinking": { - id: "gemini-2.5-flash-nothinking", - name: "Gemini 2.5 Flash (No Thinking)", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "exa-research-pro": { - id: "exa-research-pro", - name: "Exa (Research Pro)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "grok-3-fast-beta": { - id: "grok-3-fast-beta", - name: "Grok 3 Fast Beta", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "claude-opus-4-5-20251101:thinking": { - id: "claude-opus-4-5-20251101:thinking", - name: "Claude 4.5 Opus Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-pro-exp-03-25": { - id: "gemini-2.5-pro-exp-03-25", - name: "Gemini 2.5 Pro Experimental 0325", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "claude-3-7-sonnet-thinking": { - id: "claude-3-7-sonnet-thinking", - name: "Claude 3.7 Sonnet Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16000 }, - }, - "claude-opus-4-thinking:8192": { - id: "claude-opus-4-thinking:8192", - name: "Claude 4 Opus Thinking (8K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-sonnet-4-thinking:1024": { - id: "claude-sonnet-4-thinking:1024", - name: "Claude 4 Sonnet Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "Llama-3.3-70B-Magnum-v4-SE-Cirrus-x1-SLERP": { - id: "Llama-3.3-70B-Magnum-v4-SE-Cirrus-x1-SLERP", - name: "Llama 3.3 70B Magnum v4 SE Cirrus x1 SLERP", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "step-r1-v-mini": { - id: "step-r1-v-mini", - name: "Step R1 V Mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-08", - last_updated: "2025-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 11 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "ernie-x1-32k-preview": { - id: "ernie-x1-32k-preview", - name: "Ernie X1 32k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 32000, input: 32000, output: 16384 }, - }, - "Llama-3.3-70B-StrawberryLemonade-v1.0": { - id: "Llama-3.3-70B-StrawberryLemonade-v1.0", - name: "Llama 3.3 70B StrawberryLemonade v1.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "KAT-Coder-Exp-72B-1010": { - id: "KAT-Coder-Exp-72B-1010", - name: "KAT Coder Exp 72B 1010", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "gemini-2.5-pro-preview-03-25": { - id: "gemini-2.5-pro-preview-03-25", - name: "Gemini 2.5 Pro Preview 0325", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "claude-opus-4-thinking:1024": { - id: "claude-opus-4-thinking:1024", - name: "Claude 4 Opus Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude 4 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "Llama-3.3-70B-Progenitor-V3.3": { - id: "Llama-3.3-70B-Progenitor-V3.3", - name: "Llama 3.3 70B Progenitor V3.3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Qwen2.5-32B-EVA-v0.2": { - id: "Qwen2.5-32B-EVA-v0.2", - name: "Qwen 2.5 32b EVA", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.493, output: 0.493 }, - limit: { context: 24576, input: 24576, output: 8192 }, - }, - "brave-pro": { - id: "brave-pro", - name: "Brave (Pro)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-03-02", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 5 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "step-2-16k-exp": { - id: "step-2-16k-exp", - name: "Step-2 16k Exp", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-05", - last_updated: "2024-07-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.004, output: 19.992 }, - limit: { context: 16000, input: 16000, output: 8192 }, - }, - "Llama-3.3-70B-Fallen-R1-v1": { - id: "Llama-3.3-70B-Fallen-R1-v1", - name: "Llama 3.3 70B Fallen R1 v1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-sonnet-4-thinking": { - id: "claude-sonnet-4-thinking", - name: "Claude 4 Sonnet Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "doubao-1.5-pro-256k": { - id: "doubao-1.5-pro-256k", - name: "Doubao 1.5 Pro 256k", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.799, output: 1.445 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 200000, input: 200000, output: 16000 }, - }, - "learnlm-1.5-pro-experimental": { - id: "learnlm-1.5-pro-experimental", - name: "Gemini LearnLM Experimental", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.502, output: 10.506 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - chroma: { - id: "chroma", - name: "Chroma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "Llama-3.3-70B-Predatorial-Extasy": { - id: "Llama-3.3-70B-Predatorial-Extasy", - name: "Llama 3.3 70B Predatorial Extasy", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Aurora-Borealis": { - id: "Llama-3.3-70B-Aurora-Borealis", - name: "Llama 3.3 70B Aurora Borealis", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-ArliAI-RPMax-v3": { - id: "Llama-3.3-70B-ArliAI-RPMax-v3", - name: "Llama 3.3 70B ArliAI RPMax v3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "venice-uncensored": { - id: "venice-uncensored", - name: "Venice Uncensored", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "step-3": { - id: "step-3", - name: "Step-3", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2499, output: 0.6494 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.0": { - id: "Llama-3.3-70B-The-Omega-Directive-Unslop-v2.0", - name: "Llama 3.3 70B Omega Directive Unslop v2.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "auto-model": { - id: "auto-model", - name: "Auto model", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "claude-opus-4-1-thinking:32768": { - id: "claude-opus-4-1-thinking:32768", - name: "Claude 4.1 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Shakudo": { - id: "Llama-3.3-70B-Shakudo", - name: "Llama 3.3 70B Shakudo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Baichuan4-Air": { - id: "Baichuan4-Air", - name: "Baichuan 4 Air", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.157, output: 0.157 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "kimi-thinking-preview": { - id: "kimi-thinking-preview", - name: "Kimi Thinking Preview", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 31.46, output: 31.46 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04998, output: 0.2006 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "Llama-3.3-70B-Mhnnn-x1": { - id: "Llama-3.3-70B-Mhnnn-x1", - name: "Llama 3.3 70B Mhnnn x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-thinking:32768": { - id: "claude-opus-4-thinking:32768", - name: "Claude 4 Opus Thinking (32K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "Llama-3.3-70B-Argunaut-1-SFT": { - id: "Llama-3.3-70B-Argunaut-1-SFT", - name: "Llama 3.3 70B Argunaut 1 SFT", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "claude-opus-4-1-thinking:1024": { - id: "claude-opus-4-1-thinking:1024", - name: "Claude 4.1 Opus Thinking (1K)", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "phi-4-multimodal-instruct": { - id: "phi-4-multimodal-instruct", - name: "Phi 4 Multimodal", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.11 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "doubao-seed-2-0-code-preview-260215": { - id: "doubao-seed-2-0-code-preview-260215", - name: "Doubao Seed 2.0 Code Preview", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.782, output: 3.893 }, - limit: { context: 256000, input: 256000, output: 128000 }, - }, - "deepseek-reasoner-cheaper": { - id: "deepseek-reasoner-cheaper", - name: "Deepseek R1 Cheaper", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "exa-answer": { - id: "exa-answer", - name: "Exa (Answer)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 4096, input: 4096, output: 4096 }, - }, - "v0-1.0-md": { - id: "v0-1.0-md", - name: "v0 1.0 MD", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "glm-4.1v-thinking-flash": { - id: "glm-4.1v-thinking-flash", - name: "GLM 4.1V Thinking Flash", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 64000, input: 64000, output: 8192 }, - }, - "azure-o1": { - id: "azure-o1", - name: "Azure o1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-17", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 59.993 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "GLM-4.5-Air-Derestricted": { - id: "GLM-4.5-Air-Derestricted", - name: "GLM 4.5 Air Derestricted", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 202600, input: 202600, output: 98304 }, - }, - "azure-o3-mini": { - id: "azure-o3-mini", - name: "Azure o3-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.088, output: 4.3996 }, - limit: { context: 200000, input: 200000, output: 65536 }, - }, - "qwen3.6-max-preview": { - id: "qwen3.6-max-preview", - name: "Qwen3.6 Max Preview", - family: "qwen3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-04-20", - last_updated: "2026-04-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.3, output: 7.8 }, - limit: { context: 245800, output: 65536 }, - }, - "Llama-3.3-70B-Sapphira-0.2": { - id: "Llama-3.3-70B-Sapphira-0.2", - name: "Llama 3.3 70B Sapphira 0.2", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Anthrobomination": { - id: "Llama-3.3-70B-Anthrobomination", - name: "Llama 3.3 70B Anthrobomination", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "QwQ-32B-ArliAI-RpR-v1": { - id: "QwQ-32B-ArliAI-RpR-v1", - name: "QwQ 32b Arli V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude 4 Opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 14.994, output: 75.004 }, - limit: { context: 200000, input: 200000, output: 32000 }, - }, - "yi-lightning": { - id: "yi-lightning", - name: "Yi Lightning", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-16", - last_updated: "2024-10-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 12000, input: 12000, output: 4096 }, - }, - "Llama-3.3-70B-Electra-R1": { - id: "Llama-3.3-70B-Electra-R1", - name: "Llama 3.3 70B Electra R1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Forgotten-Abomination-v5.0": { - id: "Llama-3.3-70B-Forgotten-Abomination-v5.0", - name: "Llama 3.3 70B Forgotten Abomination v5.0", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Llama-3.3-70B-Cirrus-x1": { - id: "Llama-3.3-70B-Cirrus-x1", - name: "Llama 3.3 70B Cirrus x1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "grok-3-mini-beta": { - id: "grok-3-mini-beta", - name: "Grok 3 Mini Beta", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 131072, input: 131072, output: 131072 }, - }, - "auto-model-standard": { - id: "auto-model-standard", - name: "Auto model (Standard)", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 1000000, input: 1000000, output: 1000000 }, - }, - "claude-sonnet-4-5-20250929-thinking": { - id: "claude-sonnet-4-5-20250929-thinking", - name: "Claude Sonnet 4.5 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.994 }, - limit: { context: 1000000, input: 1000000, output: 64000 }, - }, - "v0-1.5-md": { - id: "v0-1.5-md", - name: "v0 1.5 MD", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-04", - last_updated: "2025-07-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, input: 200000, output: 64000 }, - }, - "kimi-k2-instruct-fast": { - id: "kimi-k2-instruct-fast", - name: "Kimi K2 0711 Fast", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-15", - last_updated: "2025-07-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "glm-4-long": { - id: "glm-4-long", - name: "GLM-4 Long", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 1000000, input: 1000000, output: 4096 }, - }, - "jamba-large-1.7": { - id: "jamba-large-1.7", - name: "Jamba Large 1.7", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.989, output: 7.99 }, - limit: { context: 256000, input: 256000, output: 4096 }, - }, - "qvq-max": { - id: "qvq-max", - name: "Qwen: QvQ Max", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-28", - last_updated: "2025-03-28", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 5.3 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "gemini-2.0-flash-thinking-exp-1219": { - id: "gemini-2.0-flash-thinking-exp-1219", - name: "Gemini 2.0 Flash Thinking 1219", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-19", - last_updated: "2024-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 32767, input: 32767, output: 8192 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0748, output: 0.306 }, - limit: { context: 1000000, input: 1000000, output: 8192 }, - }, - "azure-gpt-4-turbo": { - id: "azure-gpt-4-turbo", - name: "Azure gpt-4-turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 30.005 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "Baichuan-M2": { - id: "Baichuan-M2", - name: "Baichuan M2 32B Medical", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15.73, output: 15.73 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "qwen-long": { - id: "qwen-long", - name: "Qwen Long 10M", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.408 }, - limit: { context: 10000000, input: 10000000, output: 8192 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Perplexity Reasoning Pro", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 7.9985 }, - limit: { context: 127000, input: 127000, output: 128000 }, - }, - "gemini-2.5-flash-preview-05-20:thinking": { - id: "gemini-2.5-flash-preview-05-20:thinking", - name: "Gemini 2.5 Flash 0520 Thinking", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 3.5 }, - limit: { context: 1048000, input: 1048000, output: 65536 }, - }, - "GLM-4.5-Air-Derestricted-Steam-ReExtract": { - id: "GLM-4.5-Air-Derestricted-Steam-ReExtract", - name: "GLM 4.5 Air Derestricted Steam ReExtract", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-12", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 131072, input: 131072, output: 65536 }, - }, - "Llama-3.3-70B-Dark-Ages-v0.1": { - id: "Llama-3.3-70B-Dark-Ages-v0.1", - name: "Llama 3.3 70B Dark Ages v0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "Baichuan4-Turbo": { - id: "Baichuan4-Turbo", - name: "Baichuan 4 Turbo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.42, output: 2.42 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "doubao-1.5-vision-pro-32k": { - id: "doubao-1.5-vision-pro-32k", - name: "Doubao 1.5 Vision Pro 32k", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-22", - last_updated: "2025-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.459, output: 1.377 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "alibaba/qwen3.6-flash": { - id: "alibaba/qwen3.6-flash", - name: "Qwen3.6 Flash", - family: "qwen3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19, output: 1.16 }, - limit: { context: 991800, output: 65536 }, - }, - "inflection/inflection-3-pi": { - id: "inflection/inflection-3-pi", - name: "Inflection 3 Pi", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "inflection/inflection-3-productivity": { - id: "inflection/inflection-3-productivity", - name: "Inflection 3 Productivity", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 8000, input: 8000, output: 4096 }, - }, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "RNJ-1 Instruct 8B", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-13", - last_updated: "2025-12-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "LLM360/K2-Think": { - id: "LLM360/K2-Think", - name: "K2-Think", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "TEE/kimi-k2.5": { - id: "TEE/kimi-k2.5", - name: "Kimi K2.5 TEE", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "TEE/glm-4.7": { - id: "TEE/glm-4.7", - name: "GLM 4.7 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 3.3 }, - limit: { context: 131000, input: 131000, output: 65535 }, - }, - "TEE/qwen3.5-397b-a17b": { - id: "TEE/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-28", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 258048, input: 258048, output: 65536 }, - }, - "TEE/glm-5": { - id: "TEE/glm-5", - name: "GLM 5 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 3.5 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/qwen2.5-vl-72b-instruct": { - id: "TEE/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B TEE", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "TEE/minimax-m2.1": { - id: "TEE/minimax-m2.1", - name: "MiniMax M2.1 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "TEE/qwen3-30b-a3b-instruct-2507": { - id: "TEE/qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.44999999999999996 }, - limit: { context: 262000, input: 262000, output: 32768 }, - }, - "TEE/deepseek-v3.1": { - id: "TEE/deepseek-v3.1", - name: "DeepSeek V3.1 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2.5 }, - limit: { context: 164000, input: 164000, output: 8192 }, - }, - "TEE/llama3-3-70b": { - id: "TEE/llama3-3-70b", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "TEE/glm-4.6": { - id: "TEE/glm-4.6", - name: "GLM 4.6 TEE", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 2 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/kimi-k2.5-thinking": { - id: "TEE/kimi-k2.5-thinking", - name: "Kimi K2.5 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "TEE/gemma-3-27b-it": { - id: "TEE/gemma-3-27b-it", - name: "Gemma 3 27B TEE", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "TEE/deepseek-v3.2": { - id: "TEE/deepseek-v3.2", - name: "DeepSeek V3.2 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1 }, - limit: { context: 164000, input: 164000, output: 65536 }, - }, - "TEE/gpt-oss-20b": { - id: "TEE/gpt-oss-20b", - name: "GPT-OSS 20B TEE", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "TEE/qwen3-coder": { - id: "TEE/qwen3-coder", - name: "Qwen3 Coder 480B TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "TEE/glm-4.7-flash": { - id: "TEE/glm-4.7-flash", - name: "GLM 4.7 Flash TEE", - family: "glm-flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 203000, input: 203000, output: 65535 }, - }, - "TEE/gpt-oss-120b": { - id: "TEE/gpt-oss-120b", - name: "GPT-OSS 120B TEE", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "TEE/deepseek-r1-0528": { - id: "TEE/deepseek-r1-0528", - name: "DeepSeek R1 0528 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "TEE/kimi-k2-thinking": { - id: "TEE/kimi-k2-thinking", - name: "Kimi K2 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 2 }, - limit: { context: 128000, input: 128000, output: 65535 }, - }, - "CrucibleLab/L3.3-70B-Loki-V2.0": { - id: "CrucibleLab/L3.3-70B-Loki-V2.0", - name: "L3.3 70B Loki v2.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "deepseek/deepseek-v3.2:thinking": { - id: "deepseek/deepseek-v3.2:thinking", - name: "DeepSeek V3.2 Thinking", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "deepseek/deepseek-prover-v2-671b": { - id: "deepseek/deepseek-prover-v2-671b", - name: "DeepSeek Prover v2 671B", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2.5 }, - limit: { context: 160000, input: 160000, output: 16384 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek V3.2 Speciale", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163000, input: 163000, output: 65536 }, - }, - "Doctor-Shotgun/MS3.2-24B-Magnum-Diamond": { - id: "Doctor-Shotgun/MS3.2-24B-Magnum-Diamond", - name: "MS3.2 24B Magnum Diamond", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "NeverSleep/Llama-3-Lumimaid-70B-v0.1": { - id: "NeverSleep/Llama-3-Lumimaid-70B-v0.1", - name: "Lumimaid 70b", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "NeverSleep/Lumimaid-v0.2-70B": { - id: "NeverSleep/Lumimaid-v0.2-70B", - name: "Lumimaid v0.2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1.5 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "Steelskull/L3.3-Cu-Mai-R1-70b": { - id: "Steelskull/L3.3-Cu-Mai-R1-70b", - name: "Llama 3.3 70B Cu Mai", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-Nevoria-R1-70b": { - id: "Steelskull/L3.3-Nevoria-R1-70b", - name: "Steelskull Nevoria R1 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Evayale-70B": { - id: "Steelskull/L3.3-MS-Evayale-70B", - name: "Evayale 70b ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-Electra-R1-70b": { - id: "Steelskull/L3.3-Electra-R1-70b", - name: "Steelskull Electra R1 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.69989, output: 0.69989 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Nevoria-70b": { - id: "Steelskull/L3.3-MS-Nevoria-70b", - name: "Steelskull Nevoria 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Steelskull/L3.3-MS-Evalebis-70b": { - id: "Steelskull/L3.3-MS-Evalebis-70b", - name: "MS Evalebis 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "miromind-ai/mirothinker-v1.5-235b": { - id: "miromind-ai/mirothinker-v1.5-235b", - name: "MiroThinker v1.5 235B", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-07", - last_updated: "2026-01-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 32768, input: 32768, output: 4000 }, - }, - "pamanseau/OpenReasoning-Nemotron-32B": { - id: "pamanseau/OpenReasoning-Nemotron-32B", - name: "OpenReasoning Nemotron 32B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 32768, input: 32768, output: 65536 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Trinity Mini", - family: "trinity-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045000000000000005, output: 0.15 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "arcee-ai/trinity-large": { - id: "arcee-ai/trinity-large", - name: "Trinity Large", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "cognitivecomputations/dolphin-2.9.2-qwen2-72b": { - id: "cognitivecomputations/dolphin-2.9.2-qwen2-72b", - name: "Dolphin 72b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.306 }, - limit: { context: 8192, input: 8192, output: 4096 }, - }, - "deepcogito/cogito-v1-preview-qwen-32B": { - id: "deepcogito/cogito-v1-preview-qwen-32B", - name: "Cogito v1 Preview Qwen 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.7999999999999998, output: 1.7999999999999998 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "deepcogito/cogito-v2.1-671b": { - id: "deepcogito/cogito-v2.1-671b", - name: "Cogito v2.1 671B MoE", - family: "cogito", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "Salesforce/Llama-xLAM-2-70b-fc-r": { - id: "Salesforce/Llama-xLAM-2-70b-fc-r", - name: "Llama-xLAM-2 70B fc-r", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-13", - last_updated: "2025-04-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 2.5 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "NousResearch 2/hermes-4-405b:thinking": { - id: "NousResearch 2/hermes-4-405b:thinking", - name: "Hermes 4 Large (Thinking)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/DeepHermes-3-Mistral-24B-Preview": { - id: "NousResearch 2/DeepHermes-3-Mistral-24B-Preview", - name: "DeepHermes-3 Mistral 24B (Preview)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-10", - last_updated: "2025-05-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "NousResearch 2/Hermes-4-70B:thinking": { - id: "NousResearch 2/Hermes-4-70B:thinking", - name: "Hermes 4 (Thinking)", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-17", - last_updated: "2025-09-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.39949999999999997 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/hermes-4-405b": { - id: "NousResearch 2/hermes-4-405b", - name: "Hermes 4 Large", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "NousResearch 2/hermes-3-llama-3.1-70b": { - id: "NousResearch 2/hermes-3-llama-3.1-70b", - name: "Hermes 3 70B", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-07", - last_updated: "2026-01-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.408, output: 0.408 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "NousResearch 2/hermes-4-70b": { - id: "NousResearch 2/hermes-4-70b", - name: "Hermes 4 Medium", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.39949999999999997 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "soob3123/Veiled-Calla-12B": { - id: "soob3123/Veiled-Calla-12B", - name: "Veiled Calla 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-13", - last_updated: "2025-04-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "soob3123/GrayLine-Qwen3-8B": { - id: "soob3123/GrayLine-Qwen3-8B", - name: "Grayline Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "soob3123/amoral-gemma3-27B-v2": { - id: "soob3123/amoral-gemma3-27B-v2", - name: "Amoral Gemma3 27B v2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-05-23", - last_updated: "2025-05-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "nex-agi/deepseek-v3.1-nex-n1": { - id: "nex-agi/deepseek-v3.1-nex-n1", - name: "DeepSeek V3.1 Nex N1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-10", - last_updated: "2025-12-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "Envoid/Llama-3.05-NT-Storybreaker-Ministral-70B": { - id: "Envoid/Llama-3.05-NT-Storybreaker-Ministral-70B", - name: "Llama 3.05 Storybreaker Ministral 70b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "Envoid/Llama-3.05-Nemotron-Tenyxchat-Storybreaker-70B": { - id: "Envoid/Llama-3.05-Nemotron-Tenyxchat-Storybreaker-70B", - name: "Nemotron Tenyxchat Storybreaker 70b", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "anthracite-org/magnum-v4-72b": { - id: "anthracite-org/magnum-v4-72b", - name: "Magnum v4 72B", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.992 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "anthracite-org/magnum-v2-72b": { - id: "anthracite-org/magnum-v2-72b", - name: "Magnum V2 72B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.992 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "ReadyArt/MS3.2-The-Omega-Directive-24B-Unslop-v2.0": { - id: "ReadyArt/MS3.2-The-Omega-Directive-24B-Unslop-v2.0", - name: "Omega Directive 24B Unslop v2.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "ReadyArt/The-Omega-Abomination-L-70B-v1.0": { - id: "ReadyArt/The-Omega-Abomination-L-70B-v1.0", - name: "The Omega Abomination V1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.95 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "undi95/remm-slerp-l2-13b": { - id: "undi95/remm-slerp-l2-13b", - name: "ReMM SLERP 13B", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 1.2069999999999999 }, - limit: { context: 6144, input: 6144, output: 4096 }, - }, - "MarinaraSpaghetti/NemoMix-Unleashed-12B": { - id: "MarinaraSpaghetti/NemoMix-Unleashed-12B", - name: "NemoMix 12B Unleashed", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "allenai/molmo-2-8b": { - id: "allenai/molmo-2-8b", - name: "Molmo 2 8B", - family: "allenai", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 36864, input: 36864, output: 36864 }, - }, - "allenai/olmo-3.1-32b-instruct": { - id: "allenai/olmo-3.1-32b-instruct", - name: "Olmo 3.1 32B Instruct", - family: "allenai", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-25", - last_updated: "2026-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "allenai/olmo-3.1-32b-think": { - id: "allenai/olmo-3.1-32b-think", - name: "Olmo 3.1 32B Think", - family: "allenai", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-25", - last_updated: "2026-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "allenai/olmo-3-32b-think": { - id: "allenai/olmo-3-32b-think", - name: "Olmo 3 32B Think", - family: "allenai", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.44999999999999996 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "stepfun-ai/step-3.5-flash:thinking": { - id: "stepfun-ai/step-3.5-flash:thinking", - name: "Step 3.5 Flash Thinking", - family: "step", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.8 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-5.1": { - id: "zai-org/glm-5.1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "zai-org/glm-5.1:thinking": { - id: "zai-org/glm-5.1:thinking", - name: "GLM 5.1 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "zai-org/glm-5:thinking": { - id: "zai-org/glm-5:thinking", - name: "GLM 5 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.55 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, input: 200000, output: 128000 }, - }, - "featherless-ai/Qwerky-72B": { - id: "featherless-ai/Qwerky-72B", - name: "Qwerky 72B", - family: "qwerky", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "mlabonne/NeuralDaredevil-8B-abliterated": { - id: "mlabonne/NeuralDaredevil-8B-abliterated", - name: "Neural Daredevil 8B abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.44, output: 0.44 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "raifle/sorcererlm-8x22b": { - id: "raifle/sorcererlm-8x22b", - name: "SorcererLM 8x22B", - family: "mixtral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.505, output: 4.505 }, - limit: { context: 16000, input: 16000, output: 8192 }, - }, - "mistralai/mixtral-8x7b-instruct-v0.1": { - id: "mistralai/mixtral-8x7b-instruct-v0.1", - name: "Mixtral 8x7B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral Saba", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1989, output: 0.595 }, - limit: { context: 32000, input: 32000, output: 32768 }, - }, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3 }, - limit: { context: 262144, input: 262144, output: 256000 }, - }, - "mistralai/devstral-2-123b-instruct-2512": { - id: "mistralai/devstral-2-123b-instruct-2512", - name: "Devstral 2 123B", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.4 }, - limit: { context: 262144, input: 262144, output: 65536 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Codestral 2508", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.8999999999999999 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "mistralai/ministral-14b-instruct-2512": { - id: "mistralai/ministral-14b-instruct-2512", - name: "Ministral 3 14B", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "mistralai/mistral-tiny": { - id: "mistralai/mistral-tiny", - name: "Mistral Tiny", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-12-11", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25499999999999995, output: 0.25499999999999995 }, - limit: { context: 32000, input: 32000, output: 8192 }, - }, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Ministral 8B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "mistralai/mixtral-8x22b-instruct-v0.1": { - id: "mistralai/mixtral-8x22b-instruct-v0.1", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8999999999999999, output: 0.8999999999999999 }, - limit: { context: 65536, input: 65536, output: 32768 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, input: 131072, output: 32768 }, - }, - "mistralai/mistral-7b-instruct": { - id: "mistralai/mistral-7b-instruct", - name: "Mistral 7B Instruct", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0544, output: 0.0544 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "mistralai/Devstral-Small-2505": { - id: "mistralai/Devstral-Small-2505", - name: "Mistral Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.060000000000000005, output: 0.060000000000000005 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "mistralai/mistral-small-creative": { - id: "mistralai/mistral-small-creative", - name: "Mistral Small Creative", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large 2411", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-02-26", - last_updated: "2024-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 6.001 }, - limit: { context: 128000, input: 128000, output: 256000 }, - }, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Ministral 14B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, input: 262144, output: 32768 }, - }, - "shisa-ai/shisa-v2.1-llama3.3-70b": { - id: "shisa-ai/shisa-v2.1-llama3.3-70b", - name: "Shisa V2.1 Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 32768, input: 32768, output: 4096 }, - }, - "shisa-ai/shisa-v2-llama3.3-70b": { - id: "shisa-ai/shisa-v2-llama3.3-70b", - name: "Shisa V2 Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 0.5 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.23 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Llama 4 Scout", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.085, output: 0.46 }, - limit: { context: 328000, input: 328000, output: 65536 }, - }, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Llama 4 Maverick", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18000000000000002, output: 0.8 }, - limit: { context: 1048576, input: 1048576, output: 65536 }, - }, - "meta-llama/llama-3.2-90b-vision-instruct": { - id: "meta-llama/llama-3.2-90b-vision-instruct", - name: "Llama 3.2 Medium", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9009999999999999, output: 0.9009999999999999 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "meta-llama/llama-3.2-3b-instruct": { - id: "meta-llama/llama-3.2-3b-instruct", - name: "Llama 3.2 3b Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0306, output: 0.0493 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Llama 3.1 8b Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0544, output: 0.0544 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "GalrionSoftworks/MN-LooseCannon-12B-v1": { - id: "GalrionSoftworks/MN-LooseCannon-12B-v1", - name: "MN-LooseCannon-12B-v1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "baseten/Kimi-K2-Instruct-FP4": { - id: "baseten/Kimi-K2-Instruct-FP4", - name: "Kimi K2 0711 Instruct FP4", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "Gryphe/MythoMax-L2-13b": { - id: "Gryphe/MythoMax-L2-13b", - name: "MythoMax 13B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 4000, input: 4000, output: 4096 }, - }, - "x-ai/grok-4-fast:thinking": { - id: "x-ai/grok-4-fast:thinking", - name: "Grok 4 Fast Thinking", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-4-07-09": { - id: "x-ai/grok-4-07-09", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, input: 256000, output: 131072 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-20", - last_updated: "2025-09-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 256000, input: 256000, output: 131072 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "x-ai/grok-4.1-fast-reasoning": { - id: "x-ai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, input: 2000000, output: 131072 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "Hunyuan MT 7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 20 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "WizardLM-2 8x22B", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "microsoft/MAI-DS-R1-FP8": { - id: "microsoft/MAI-DS-R1-FP8", - name: "Microsoft DeepSeek R1", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "cohere/command-r": { - id: "cohere/command-r", - name: "Cohere: Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-03-11", - last_updated: "2024-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.476, output: 1.428 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.856, output: 14.246 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "chutesai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "chutesai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24b Instruct", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "nvidia/Llama-3.1-Nemotron-Ultra-253B-v1": { - id: "nvidia/Llama-3.1-Nemotron-Ultra-253B-v1", - name: "Nvidia Nemotron Ultra 253B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-03", - last_updated: "2025-07-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.8 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "Nvidia Nemotron 3 Nano 30B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B v2", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF": { - id: "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", - name: "Nvidia Nemotron 70b", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.357, output: 0.408 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "nvidia/Llama-3.3-Nemotron-Super-49B-v1": { - id: "nvidia/Llama-3.3-Nemotron-Super-49B-v1", - name: "Nvidia Nemotron Super 49B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "nvidia/Llama-3_3-Nemotron-Super-49B-v1_5": { - id: "nvidia/Llama-3_3-Nemotron-Super-49B-v1_5", - name: "Nvidia Nemotron Super 49B v1.5", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "TheDrummer 2/Anubis-70B-v1": { - id: "TheDrummer 2/Anubis-70B-v1", - name: "Anubis 70B v1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 65536, input: 65536, output: 16384 }, - }, - "TheDrummer 2/Cydonia-24B-v4.3": { - id: "TheDrummer 2/Cydonia-24B-v4.3", - name: "The Drummer Cydonia 24B v4.3", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-25", - last_updated: "2025-12-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "TheDrummer 2/Magidonia-24B-v4.3": { - id: "TheDrummer 2/Magidonia-24B-v4.3", - name: "The Drummer Magidonia 24B v4.3", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-25", - last_updated: "2025-12-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "TheDrummer 2/Cydonia-24B-v4": { - id: "TheDrummer 2/Cydonia-24B-v4", - name: "The Drummer Cydonia 24B v4", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2414 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/Anubis-70B-v1.1": { - id: "TheDrummer 2/Anubis-70B-v1.1", - name: "Anubis 70B v1.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "TheDrummer 2/Rocinante-12B-v1.1": { - id: "TheDrummer 2/Rocinante-12B-v1.1", - name: "Rocinante 12b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.408, output: 0.595 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "TheDrummer 2/Cydonia-24B-v4.1": { - id: "TheDrummer 2/Cydonia-24B-v4.1", - name: "The Drummer Cydonia 24B v4.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/UnslopNemo-12B-v4.1": { - id: "TheDrummer 2/UnslopNemo-12B-v4.1", - name: "UnslopNemo 12b v4", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "TheDrummer 2/Cydonia-24B-v2": { - id: "TheDrummer 2/Cydonia-24B-v2", - name: "The Drummer Cydonia 24B v2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1207 }, - limit: { context: 16384, input: 16384, output: 32768 }, - }, - "TheDrummer 2/skyfall-36b-v2": { - id: "TheDrummer 2/skyfall-36b-v2", - name: "TheDrummer Skyfall 36B V2", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 64000, input: 64000, output: 32768 }, - }, - "deepseek-ai/DeepSeek-V3.1:thinking": { - id: "deepseek-ai/DeepSeek-V3.1:thinking", - name: "DeepSeek V3.1 Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus:thinking": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus:thinking", - name: "DeepSeek V3.1 Terminus (Thinking)", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "deepseek-ai/deepseek-v3.2-exp-thinking": { - id: "deepseek-ai/deepseek-v3.2-exp-thinking", - name: "DeepSeek V3.2 Exp Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163840, input: 163840, output: 65536 }, - }, - "deepseek-ai/deepseek-v3.2-exp": { - id: "deepseek-ai/deepseek-v3.2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27999999999999997, output: 0.42000000000000004 }, - limit: { context: 163840, input: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.7 }, - limit: { context: 128000, input: 128000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 20 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT 5.2 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "GPT-4o mini Search Preview", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.088, output: 0.35 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/chatgpt-4o-latest": { - id: "openai/chatgpt-4o-latest", - name: "ChatGPT 4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 14.993999999999998 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT 5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT 5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT 5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI o3-mini (High)", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.64, output: 2.588 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1496, output: 0.595 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI o4-mini Deep Research", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT 5.1 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI o4-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT 5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT 5.1 Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o1-preview": { - id: "openai/o1-preview", - name: "OpenAI o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.993999999999998, output: 59.993 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-08-06", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT 5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2024-12-17", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 14.993999999999998, output: 59.993 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5 Turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2022-11-30", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, input: 16385, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI o3 Deep Research", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "GPT-4 Turbo Preview", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2023-11-06", - last_updated: "2024-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 30.004999999999995 }, - limit: { context: 128000, input: 128000, output: 4096 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "OpenAI o1 Pro", - family: "o-pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "openai/gpt-5.1-chat-latest": { - id: "openai/gpt-5.1-chat-latest", - name: "GPT 5.1 Chat (Latest)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 16384 }, - }, - "openai/gpt-4o-search-preview": { - id: "openai/gpt-4o-search-preview", - name: "GPT-4o Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.47, output: 5.88 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT 4.1 Nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI o4-mini high", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT 5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-5.1-2025-11-13": { - id: "openai/gpt-5.1-2025-11-13", - name: "GPT-5.1 (2025-11-13)", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1000000, input: 1000000, output: 32768 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.499, output: 9.996 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o3-mini-low": { - id: "openai/o3-mini-low", - name: "OpenAI o3-mini (Low)", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT 5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/o3-pro-2025-06-10": { - id: "openai/o3-pro-2025-06-10", - name: "OpenAI o3-pro (2025-06-10)", - family: "o-pro", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9.996, output: 19.992 }, - limit: { context: 200000, input: 200000, output: 100000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "openai/gpt-5-chat-latest": { - id: "openai/gpt-5-chat-latest", - name: "GPT 5 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT 4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT 4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1047576, input: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT 5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 400000, output: 128000 }, - }, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, input: 128000, output: 16384 }, - }, - "VongolaChouko/Starcannon-Unleashed-12B-v1.0": { - id: "VongolaChouko/Starcannon-Unleashed-12B-v1.0", - name: "Mistral Nemo Starcannon 12b v1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon Nova Lite 1.0", - family: "nova-lite", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0595, output: 0.238 }, - limit: { context: 300000, input: 300000, output: 5120 }, - }, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon Nova Pro 1.0", - family: "nova-pro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 3.1959999999999997 }, - limit: { context: 300000, input: 300000, output: 32000 }, - }, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon Nova 2 Lite", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5099999999999999, output: 4.25 }, - limit: { context: 1000000, input: 1000000, output: 65535 }, - }, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon Nova Micro 1.0", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0357, output: 0.1394 }, - limit: { context: 128000, input: 128000, output: 5120 }, - }, - "Sao10K/L3.3-70B-Euryale-v2.3": { - id: "Sao10K/L3.3-70B-Euryale-v2.3", - name: "Llama 3.3 70B Euryale", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 20480, input: 20480, output: 16384 }, - }, - "Sao10K/L3.1-70B-Euryale-v2.2": { - id: "Sao10K/L3.1-70B-Euryale-v2.2", - name: "Llama 3.1 70B Euryale", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.306, output: 0.357 }, - limit: { context: 20480, input: 20480, output: 16384 }, - }, - "Sao10K/L3.1-70B-Hanami-x1": { - id: "Sao10K/L3.1-70B-Hanami-x1", - name: "Llama 3.1 70B Hanami", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "Sao10K/L3-8B-Stheno-v3.2": { - id: "Sao10K/L3-8B-Stheno-v3.2", - name: "Sao10K Stheno 8b", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "LatitudeGames/Wayfarer-Large-70B-Llama-3.3": { - id: "LatitudeGames/Wayfarer-Large-70B-Llama-3.3", - name: "Llama 3.3 70B Wayfarer", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.700000007, output: 0.700000007 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "z-ai/glm-4.6:thinking": { - id: "z-ai/glm-4.6:thinking", - name: "GLM 4.6 Thinking", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 200000, input: 200000, output: 65535 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-22", - last_updated: "2025-11-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 1.7999999999999998 }, - limit: { context: 64000, input: 64000, output: 96000 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 200000, input: 200000, output: 65535 }, - }, - "z-ai/glm-4.5v:thinking": { - id: "z-ai/glm-4.5v:thinking", - name: "GLM 4.5V Thinking", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-22", - last_updated: "2025-11-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 1.7999999999999998 }, - limit: { context: 64000, input: 64000, output: 96000 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "ERNIE 4.5 VL 28B", - family: "ernie", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13999999999999999, output: 0.5599999999999999 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "baidu/ernie-4.5-300b-a47b": { - id: "baidu/ernie-4.5-300b-a47b", - name: "ERNIE 4.5 300B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.15 }, - limit: { context: 131072, input: 131072, output: 16384 }, - }, - "dmind/dmind-1": { - id: "dmind/dmind-1", - name: "DMind-1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.6 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "dmind/dmind-1-mini": { - id: "dmind/dmind-1-mini", - name: "DMind-1-Mini", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.4 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "Infermatic/MN-12B-Inferor-v0.0": { - id: "Infermatic/MN-12B-Inferor-v0.0", - name: "Mistral Nemo Inferor 12B", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25499999999999995, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "meituan-longcat/LongCat-Flash-Chat-FP8": { - id: "meituan-longcat/LongCat-Flash-Chat-FP8", - name: "LongCat Flash", - family: "longcat", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-08-31", - last_updated: "2025-08-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.7 }, - limit: { context: 128000, input: 128000, output: 32768 }, - }, - "meganova-ai/manta-mini-1.0": { - id: "meganova-ai/manta-mini-1.0", - name: "Manta Mini 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.16 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - "meganova-ai/manta-pro-1.0": { - id: "meganova-ai/manta-pro-1.0", - name: "Manta Pro 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.060000000000000005, output: 0.5 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "meganova-ai/manta-flash-1.0": { - id: "meganova-ai/manta-flash-1.0", - name: "Manta Flash 1.0", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-20", - last_updated: "2025-12-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.16 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, input: 204800, output: 131072 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax 01", - family: "minimax", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1394, output: 1.1219999999999999 }, - limit: { context: 1000192, input: 1000192, output: 16384 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 1.32 }, - limit: { context: 200000, input: 200000, output: 131072 }, - }, - "minimax/minimax-m2-her": { - id: "minimax/minimax-m2-her", - name: "MiniMax M2-her", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-01-24", - last_updated: "2026-01-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.30200000000000005, output: 1.2069999999999999 }, - limit: { context: 65532, input: 65532, output: 2048 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, input: 204800, output: 131072 }, - }, - "qwen/Qwen3.6-35B-A3B:thinking": { - id: "qwen/Qwen3.6-35B-A3B:thinking", - name: "Qwen3.6 35B A3B Thinking", - family: "qwen3.6", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2026-04-19", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.74 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 258048, input: 258048, output: 65536 }, - }, - "qwen/Qwen3.6-35B-A3B": { - id: "qwen/Qwen3.6-35B-A3B", - name: "Qwen3.6 35B A3B", - family: "qwen3.6", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2026-04-17", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.74 }, - limit: { context: 262144, output: 16384 }, - }, - "unsloth/gemma-3-1b-it": { - id: "unsloth/gemma-3-1b-it", - name: "Gemma 3 1B IT", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1003, output: 0.1003 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "unsloth/gemma-3-12b-it": { - id: "unsloth/gemma-3-12b-it", - name: "Gemma 3 12B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.272, output: 0.272 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "unsloth/gemma-3-4b-it": { - id: "unsloth/gemma-3-4b-it", - name: "Gemma 3 4B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "unsloth/gemma-3-27b-it": { - id: "unsloth/gemma-3-27b-it", - name: "Gemma 3 27B IT", - family: "unsloth", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2992, output: 0.2992 }, - limit: { context: 128000, input: 128000, output: 96000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "GLM Z1 9B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 8000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "GLM 4 9B 0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 8000 }, - }, - "THUDM/GLM-Z1-Rumination-32B-0414": { - id: "THUDM/GLM-Z1-Rumination-32B-0414", - name: "GLM Z1 Rumination 32B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 32000, input: 32000, output: 65536 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "GLM 4 32B 0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "GLM Z1 32B 0414", - family: "glm-z", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "google/gemini-flash-1.5": { - id: "google/gemini-flash-1.5", - name: "Gemini 1.5 Flash", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0748, output: 0.306 }, - limit: { context: 2000000, input: 2000000, output: 8192 }, - }, - "google/gemini-3-flash-preview-thinking": { - id: "google/gemini-3-flash-preview-thinking", - name: "Gemini 3 Flash Thinking", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048756, input: 1048756, output: 65536 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 256000, input: 256000, output: 65536 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "moonshotai/kimi-k2-thinking-original": { - id: "moonshotai/kimi-k2-thinking-original", - name: "Kimi K2 Thinking Original", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "moonshotai/kimi-k2-instruct-0711": { - id: "moonshotai/kimi-k2-instruct-0711", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 2 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "moonshotai/Kimi-Dev-72B": { - id: "moonshotai/Kimi-Dev-72B", - name: "Kimi Dev 72B", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 128000, input: 128000, output: 131072 }, - }, - "moonshotai/kimi-k2-thinking-turbo-original": { - id: "moonshotai/kimi-k2-thinking-turbo-original", - name: "Kimi K2 Thinking Turbo Original", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8 }, - limit: { context: 256000, input: 256000, output: 16384 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - release_date: "2026-04-16", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.53, output: 2.73 }, - limit: { context: 256000, output: 65536 }, - }, - "moonshotai/kimi-k2.6:thinking": { - id: "moonshotai/kimi-k2.6:thinking", - name: "Kimi K2.6 Thinking", - family: "kimi-thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - release_date: "2026-04-16", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.53, output: 2.73 }, - limit: { context: 256000, output: 65536 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 256000, input: 256000, output: 262144 }, - }, - "moonshotai/kimi-k2.5:thinking": { - id: "moonshotai/kimi-k2.5:thinking", - name: "Kimi K2.5 Thinking", - family: "kimi-thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.9 }, - limit: { context: 256000, input: 256000, output: 65536 }, - }, - "Tongyi-Zhiwen/QwenLong-L1-32B": { - id: "Tongyi-Zhiwen/QwenLong-L1-32B", - name: "QwenLong L1 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13999999999999999, output: 0.6 }, - limit: { context: 128000, input: 128000, output: 40960 }, - }, - "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16": { - id: "nothingiisreal/L3.1-70B-Celeste-V0.1-BF16", - name: "Llama 3.1 70B Celeste v0.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "aion-labs/aion-1.0": { - id: "aion-labs/aion-1.0", - name: "Aion 1.0", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3.995, output: 7.99 }, - limit: { context: 65536, input: 65536, output: 8192 }, - }, - "aion-labs/aion-rp-llama-3.1-8b": { - id: "aion-labs/aion-rp-llama-3.1-8b", - name: "Llama 3.1 8b (uncensored)", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2006, output: 0.2006 }, - limit: { context: 32768, input: 32768, output: 16384 }, - }, - "aion-labs/aion-1.0-mini": { - id: "aion-labs/aion-1.0-mini", - name: "Aion 1.0 mini (DeepSeek)", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 1.394 }, - limit: { context: 131072, input: 131072, output: 8192 }, - }, - "Alibaba-NLP/Tongyi-DeepResearch-30B-A3B": { - id: "Alibaba-NLP/Tongyi-DeepResearch-30B-A3B", - name: "Tongyi DeepResearch 30B A3B", - family: "yi", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.24000000000000002 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "MiniMaxAI/MiniMax-M1-80k": { - id: "MiniMaxAI/MiniMax-M1-80k", - name: "MiniMax M1 80K", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-06-16", - last_updated: "2025-06-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6052, output: 2.4225000000000003 }, - limit: { context: 1000000, input: 1000000, output: 131072 }, - }, - "anthropic/claude-opus-4.6:thinking:low": { - id: "anthropic/claude-opus-4.6:thinking:low", - name: "Claude 4.6 Opus Thinking Low", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude 4.6 Opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4.6:thinking": { - id: "anthropic/claude-sonnet-4.6:thinking", - name: "Claude Sonnet 4.6 Thinking", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.993999999999998 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking:max": { - id: "anthropic/claude-opus-4.6:thinking:max", - name: "Claude 4.6 Opus Thinking Max", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking:medium": { - id: "anthropic/claude-opus-4.6:thinking:medium", - name: "Claude 4.6 Opus Thinking Medium", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.992, output: 14.993999999999998 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.6:thinking": { - id: "anthropic/claude-opus-4.6:thinking", - name: "Claude 4.6 Opus Thinking", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.998, output: 25.007 }, - limit: { context: 1000000, input: 1000000, output: 128000 }, - }, - "abacusai/Dracarys-72B-Instruct": { - id: "abacusai/Dracarys-72B-Instruct", - name: "Llama 3.1 70B Dracarys 2", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-02", - last_updated: "2025-08-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.0": { - id: "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.0", - name: "EVA Llama 3.33 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "EVA-UNIT-01/EVA-Qwen2.5-72B-v0.2": { - id: "EVA-UNIT-01/EVA-Qwen2.5-72B-v0.2", - name: "EVA-Qwen2.5-72B-v0.2", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 0.7989999999999999 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.1": { - id: "EVA-UNIT-01/EVA-LLaMA-3.33-70B-v0.1", - name: "EVA-LLaMA-3.33-70B-v0.1", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.006, output: 2.006 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "EVA-UNIT-01/EVA-Qwen2.5-32B-v0.2": { - id: "EVA-UNIT-01/EVA-Qwen2.5-32B-v0.2", - name: "EVA-Qwen2.5-32B-v0.2", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7989999999999999, output: 0.7989999999999999 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated": { - id: "huihui-ai/DeepSeek-R1-Distill-Qwen-32B-abliterated", - name: "DeepSeek R1 Qwen Abliterated", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 1.4 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/DeepSeek-R1-Distill-Llama-70B-abliterated": { - id: "huihui-ai/DeepSeek-R1-Distill-Llama-70B-abliterated", - name: "DeepSeek R1 Llama 70B Abliterated", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "huihui-ai/Llama-3.3-70B-Instruct-abliterated": { - id: "huihui-ai/Llama-3.3-70B-Instruct-abliterated", - name: "Llama 3.3 70B Instruct abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "huihui-ai/Qwen2.5-32B-Instruct-abliterated": { - id: "huihui-ai/Qwen2.5-32B-Instruct-abliterated", - name: "Qwen 2.5 32B Abliterated", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-01-06", - last_updated: "2025-01-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32768, input: 32768, output: 8192 }, - }, - "huihui-ai/Llama-3.1-Nemotron-70B-Instruct-HF-abliterated": { - id: "huihui-ai/Llama-3.1-Nemotron-70B-Instruct-HF-abliterated", - name: "Nemotron 3.1 70B abliterated", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 16384, input: 16384, output: 16384 }, - }, - "xiaomi/mimo-v2-flash-thinking-original": { - id: "xiaomi/mimo-v2-flash-thinking-original", - name: "MiMo V2 Flash (Thinking) Original", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash-thinking": { - id: "xiaomi/mimo-v2-flash-thinking", - name: "MiMo V2 Flash (Thinking)", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "xiaomi/mimo-v2-flash-original": { - id: "xiaomi/mimo-v2-flash-original", - name: "MiMo V2 Flash Original", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.102, output: 0.306 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "tngtech/DeepSeek-TNG-R1T2-Chimera": { - id: "tngtech/DeepSeek-TNG-R1T2-Chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "tngtech", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.31, output: 0.31 }, - limit: { context: 128000, input: 128000, output: 8192 }, - }, - "tngtech/tng-r1t-chimera": { - id: "tngtech/tng-r1t-chimera", - name: "TNG R1T Chimera", - family: "tngtech", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 128000, input: 128000, output: 65536 }, - }, - "inflatebot/MN-12B-Mag-Mell-R1": { - id: "inflatebot/MN-12B-Mag-Mell-R1", - name: "Mag Mell R1", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.49299999999999994, output: 0.49299999999999994 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - "failspy/Meta-Llama-3-70B-Instruct-abliterated-v3.5": { - id: "failspy/Meta-Llama-3-70B-Instruct-abliterated-v3.5", - name: "Llama 3 70B abliterated", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 8192, input: 8192, output: 8192 }, - }, - }, - }, - abacus: { - id: "abacus", - env: ["ABACUS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://routellm.abacus.ai/v1", - name: "Abacus", - doc: "https://abacus.ai/help/api", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 32768 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048576, output: 65536 }, - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 16384 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 40 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 131072, output: 16384 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.3-codex-xhigh": { - id: "gpt-5.3-codex-xhigh", - name: "GPT-5.3 Codex XHigh", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, output: 16384 }, - }, - "route-llm": { - id: "route-llm", - name: "Route LLM", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen-2.5-coder-32b": { - id: "qwen-2.5-coder-32b", - name: "Qwen 2.5 Coder 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.79, output: 0.79 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat Latest", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo Preview", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 8 }, - limit: { context: 256000, output: 8192 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 200000, output: 128000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 Nano", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1047576, output: 32768 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 64000 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 2000000, output: 16384 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.66 }, - limit: { context: 128000, output: 8192 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-11-28", - last_updated: "2024-11-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262144, output: 8192 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29 }, - limit: { context: 128000, output: 8192 }, - }, - "Qwen/qwen3-coder-480b-a35b-instruct": { - id: "Qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.2 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen 2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.38 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "zai-org/glm-4.6": { - id: "zai-org/glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 8192 }, - }, - "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": { - id: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", - name: "Llama 3.1 405B Instruct Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.5, output: 3.5 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.59 }, - limit: { context: 1000000, output: 32768 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 7 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-15", - last_updated: "2025-06-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.44 }, - limit: { context: 128000, output: 32768 }, - }, - }, - }, - "perplexity-agent": { - id: "perplexity-agent", - env: ["PERPLEXITY_API_KEY"], - npm: "@ai-sdk/openai", - api: "https://api.perplexity.ai/v1", - name: "Perplexity Agent", - doc: "https://docs.perplexity.ai/docs/agent-api/models", - models: { - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2.5, cache_read: 0.0625 }, - limit: { context: 128000, output: 8192 }, - }, - "xai/grok-4-1-fast-non-reasoning": { - id: "xai/grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2.5 }, - limit: { context: 1000000, output: 32000 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-7": { - id: "anthropic/claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5 }, - limit: { context: 200000, output: 128000 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - "siliconflow-cn": { - id: "siliconflow-cn", - env: ["SILICONFLOW_CN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.siliconflow.cn/v1", - name: "SiliconFlow (China)", - doc: "https://cloud.siliconflow.com/models", - models: { - "Kwaipilot/KAT-Dev": { - id: "Kwaipilot/KAT-Dev", - name: "Kwaipilot/KAT-Dev", - family: "kat-coder", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-27", - last_updated: "2026-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 128000 }, - }, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen/Qwen3.5-397B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.74 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-35B-A3B": { - id: "Qwen/Qwen3.5-35B-A3B", - name: "Qwen/Qwen3.5-35B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.23, output: 1.86 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-122B-A10B": { - id: "Qwen/Qwen3.5-122B-A10B", - name: "Qwen/Qwen3.5-122B-A10B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 2.32 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-9B": { - id: "Qwen/Qwen3.5-9B", - name: "Qwen/Qwen3.5-9B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.74 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-27B": { - id: "Qwen/Qwen3.5-27B", - name: "Qwen/Qwen3.5-27B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.09 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.5-4B": { - id: "Qwen/Qwen3.5-4B", - name: "Qwen/Qwen3.5-4B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3.6-35B-A3B": { - id: "Qwen/Qwen3.6-35B-A3B", - name: "Qwen/Qwen3.6-35B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.23, output: 1.86 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen/Qwen2.5-72B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Instruct": { - id: "Qwen/Qwen3-VL-8B-Instruct", - name: "Qwen/Qwen3-VL-8B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.68 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-32B-Instruct": { - id: "Qwen/Qwen3-VL-32B-Instruct", - name: "Qwen/Qwen3-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Thinking": { - id: "Qwen/Qwen3-VL-30B-A3B-Thinking", - name: "Qwen/Qwen3-VL-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-14B-Instruct": { - id: "Qwen/Qwen2.5-14B-Instruct", - name: "Qwen/Qwen2.5-14B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen/Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen/Qwen2.5-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Thinking": { - id: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - name: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen/Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-32B-Instruct": { - id: "Qwen/Qwen2.5-32B-Instruct", - name: "Qwen/Qwen2.5-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-72B-Instruct-128K": { - id: "Qwen/Qwen2.5-72B-Instruct-128K", - name: "Qwen/Qwen2.5-72B-Instruct-128K", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen/Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - name: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen/Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen/Qwen3-235B-A22B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen/Qwen3-30B-A3B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-8B": { - id: "Qwen/Qwen3-8B", - name: "Qwen/Qwen3-8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Thinking": { - id: "Qwen/Qwen3-VL-8B-Thinking", - name: "Qwen/Qwen3-VL-8B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Captioner": { - id: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - name: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "Qwen/QwQ-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-06", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.58 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen/Qwen3-VL-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen/Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-7B-Instruct": { - id: "Qwen/Qwen2.5-7B-Instruct", - name: "Qwen/Qwen2.5-7B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Thinking": { - id: "Qwen/Qwen3-VL-235B-A22B-Thinking", - name: "Qwen/Qwen3-VL-235B-A22B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen/Qwen3-30B-A3B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 131000 }, - }, - "Qwen/Qwen3-VL-32B-Thinking": { - id: "Qwen/Qwen3-VL-32B-Thinking", - name: "Qwen/Qwen3-VL-32B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen/Qwen2.5-VL-72B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "stepfun-ai/Step-3.5-Flash": { - id: "stepfun-ai/Step-3.5-Flash", - name: "stepfun-ai/Step-3.5-Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "zai-org/GLM-4.5V": { - id: "zai-org/GLM-4.5V", - name: "zai-org/GLM-4.5V", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 66000, output: 66000 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "zai-org/GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.9 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "zai-org/GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-07", - last_updated: "2025-12-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "zai-org/GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-flash-2.0": { - id: "inclusionAI/Ling-flash-2.0", - name: "inclusionAI/Ling-flash-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-mini-2.0": { - id: "inclusionAI/Ling-mini-2.0", - name: "inclusionAI/Ling-mini-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ring-flash-2.0": { - id: "inclusionAI/Ring-flash-2.0", - name: "inclusionAI/Ring-flash-2.0", - family: "ring", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "ascend-tribe/pangu-pro-moe": { - id: "ascend-tribe/pangu-pro-moe", - name: "ascend-tribe/pangu-pro-moe", - family: "pangu", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2026-01-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 128000 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "tencent/Hunyuan-MT-7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 33000, output: 33000 }, - }, - "tencent/Hunyuan-A13B-Instruct": { - id: "tencent/Hunyuan-A13B-Instruct", - name: "tencent/Hunyuan-A13B-Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Pro/zai-org/GLM-4.7": { - id: "Pro/zai-org/GLM-4.7", - name: "Pro/zai-org/GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/zai-org/GLM-5.1": { - id: "Pro/zai-org/GLM-5.1", - name: "Pro/zai-org/GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_write: 0 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/zai-org/GLM-5": { - id: "Pro/zai-org/GLM-5", - name: "Pro/zai-org/GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 205000, output: 205000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3": { - id: "Pro/deepseek-ai/DeepSeek-V3", - name: "Pro/deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-R1": { - id: "Pro/deepseek-ai/DeepSeek-R1", - name: "Pro/deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3.2": { - id: "Pro/deepseek-ai/DeepSeek-V3.2", - name: "Pro/deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "Pro/deepseek-ai/DeepSeek-V3.1-Terminus", - name: "Pro/deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "Pro/moonshotai/Kimi-K2-Thinking": { - id: "Pro/moonshotai/Kimi-K2-Thinking", - name: "Pro/moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/moonshotai/Kimi-K2.6": { - id: "Pro/moonshotai/Kimi-K2.6", - name: "Pro/moonshotai/Kimi-K2.6", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/moonshotai/Kimi-K2-Instruct-0905": { - id: "Pro/moonshotai/Kimi-K2-Instruct-0905", - name: "Pro/moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/moonshotai/Kimi-K2.5": { - id: "Pro/moonshotai/Kimi-K2.5", - name: "Pro/moonshotai/Kimi-K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.25 }, - limit: { context: 262000, output: 262000 }, - }, - "Pro/MiniMaxAI/MiniMax-M2.5": { - id: "Pro/MiniMaxAI/MiniMax-M2.5", - name: "Pro/MiniMaxAI/MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.22 }, - limit: { context: 192000, output: 131000 }, - }, - "Pro/MiniMaxAI/MiniMax-M2.1": { - id: "Pro/MiniMaxAI/MiniMax-M2.1", - name: "Pro/MiniMaxAI/MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "PaddlePaddle/PaddleOCR-VL": { - id: "PaddlePaddle/PaddleOCR-VL", - name: "PaddlePaddle/PaddleOCR-VL", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 16384 }, - }, - "PaddlePaddle/PaddleOCR-VL-1.5": { - id: "PaddlePaddle/PaddleOCR-VL-1.5", - name: "PaddlePaddle/PaddleOCR-VL-1.5", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 16384 }, - }, - "deepseek-ai/DeepSeek-OCR": { - id: "deepseek-ai/DeepSeek-OCR", - name: "deepseek-ai/DeepSeek-OCR", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2025-10-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/deepseek-vl2": { - id: "deepseek-ai/deepseek-vl2", - name: "deepseek-ai/deepseek-vl2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 4000, output: 4000 }, - }, - "baidu/ERNIE-4.5-300B-A47B": { - id: "baidu/ERNIE-4.5-300B-A47B", - name: "baidu/ERNIE-4.5-300B-A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "THUDM/GLM-Z1-32B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "THUDM/GLM-4-32B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "THUDM/GLM-4-9B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "THUDM/GLM-Z1-9B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "ByteDance-Seed/Seed-OSS-36B-Instruct": { - id: "ByteDance-Seed/Seed-OSS-36B-Instruct", - name: "ByteDance-Seed/Seed-OSS-36B-Instruct", - family: "seed", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - submodel: { - id: "submodel", - env: ["SUBMODEL_INSTAGEN_ACCESS_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://llm.submodel.ai/v1", - name: "submodel", - doc: "https://submodel.gitbook.io", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 131072 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "zai-org/GLM-4.5-FP8": { - id: "zai-org/GLM-4.5-FP8", - name: "GLM 4.5 FP8", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 75000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 75000, output: 163840 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.15 }, - limit: { context: 75000, output: 163840 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-23", - last_updated: "2025-08-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - }, - }, - "minimax-coding-plan": { - id: "minimax-coding-plan", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimax.io/anthropic/v1", - name: "MiniMax Coding Plan (minimax.io)", - doc: "https://platform.minimax.io/docs/coding-plan/intro", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - perplexity: { - id: "perplexity", - env: ["PERPLEXITY_API_KEY"], - npm: "@ai-sdk/perplexity", - name: "Perplexity", - doc: "https://docs.perplexity.ai", - models: { - "sonar-pro": { - id: "sonar-pro", - name: "Sonar Pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Sonar Deep Research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-02-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, reasoning: 3 }, - limit: { context: 128000, output: 32768 }, - }, - sonar: { - id: "sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 4096 }, - }, - }, - }, - deepseek: { - id: "deepseek", - env: ["DEEPSEEK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.deepseek.com", - name: "DeepSeek", - doc: "https://api-docs.deepseek.com/quick_start/pricing", - models: { - "deepseek-chat": { - id: "deepseek-chat", - name: "DeepSeek Chat", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - family: "deepseek-thinking", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2026-02-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - llama: { - id: "llama", - env: ["LLAMA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.llama.com/compat/v1/", - name: "Llama", - doc: "https://llama.developer.meta.com/docs/models", - models: { - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cerebras-llama-4-maverick-17b-128e-instruct": { - id: "cerebras-llama-4-maverick-17b-128e-instruct", - name: "Cerebras-Llama-4-Maverick-17B-128E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-3.3-8b-instruct": { - id: "llama-3.3-8b-instruct", - name: "Llama-3.3-8B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cerebras-llama-4-scout-17b-16e-instruct": { - id: "cerebras-llama-4-scout-17b-16e-instruct", - name: "Cerebras-Llama-4-Scout-17B-16E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "groq-llama-4-maverick-17b-128e-instruct": { - id: "groq-llama-4-maverick-17b-128e-instruct", - name: "Groq-Llama-4-Maverick-17B-128E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-scout-17b-16e-instruct-fp8": { - id: "llama-4-scout-17b-16e-instruct-fp8", - name: "Llama-4-Scout-17B-16E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - }, - }, - openrouter: { - id: "openrouter", - env: ["OPENROUTER_API_KEY"], - npm: "@openrouter/ai-sdk-provider", - api: "https://openrouter.ai/api/v1", - name: "OpenRouter", - doc: "https://openrouter.ai/models", - models: { - "liquid/lfm-2.5-1.2b-instruct:free": { - id: "liquid/lfm-2.5-1.2b-instruct:free", - name: "LFM2.5-1.2B-Instruct (free)", - family: "liquid", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-20", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "liquid/lfm-2.5-1.2b-thinking:free": { - id: "liquid/lfm-2.5-1.2b-thinking:free", - name: "LFM2.5-1.2B-Thinking (free)", - family: "liquid", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-20", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-23", - last_updated: "2025-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek V3.2 Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.4 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1-terminus:exacto": { - id: "deepseek/deepseek-v3.1-terminus:exacto", - name: "DeepSeek V3.1 Terminus (exacto)", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16384, output: 8192 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "openrouter/owl-alpha": { - id: "openrouter/owl-alpha", - name: "Owl Alpha", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1048756, output: 262144 }, - status: "alpha", - }, - "openrouter/pareto-code": { - id: "openrouter/pareto-code", - name: "Pareto Code Router", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "openrouter/elephant-alpha": { - id: "openrouter/elephant-alpha", - name: "Elephant (free)", - family: "elephant", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-13", - last_updated: "2026-04-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, input: 200000, output: 8000 }, - }, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.85 }, - limit: { context: 262144, output: 80000 }, - }, - "arcee-ai/trinity-large-preview:free": { - id: "arcee-ai/trinity-large-preview:free", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "cognitivecomputations/dolphin-mistral-24b-venice-edition:free": { - id: "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", - name: "Uncensored (free)", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-07-09", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "bytedance-seed/seedream-4.5": { - id: "bytedance-seed/seedream-4.5", - name: "Seedream 4.5", - family: "seed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-23", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 4096 }, - }, - "black-forest-labs/flux.2-max": { - id: "black-forest-labs/flux.2-max", - name: "FLUX.2 Max", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-16", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 46864, output: 46864 }, - }, - "black-forest-labs/flux.2-flex": { - id: "black-forest-labs/flux.2-flex", - name: "FLUX.2 Flex", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-25", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 67344, output: 67344 }, - }, - "black-forest-labs/flux.2-pro": { - id: "black-forest-labs/flux.2-pro", - name: "FLUX.2 Pro", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-11-25", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 46864, output: 46864 }, - }, - "black-forest-labs/flux.2-klein-4b": { - id: "black-forest-labs/flux.2-klein-4b", - name: "FLUX.2 Klein 4B", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-14", - last_updated: "2026-01-31", - modalities: { input: ["image", "text"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 40960, output: 40960 }, - }, - "nousresearch/hermes-3-llama-3.1-405b:free": { - id: "nousresearch/hermes-3-llama-3.1-405b:free", - name: "Hermes 3 405B Instruct (free)", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-4-405b": { - id: "nousresearch/hermes-4-405b", - name: "Hermes 4 405B", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-4-70b": { - id: "nousresearch/hermes-4-70b", - name: "Hermes 4 70B", - family: "hermes", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Step 3.5 Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, output: 256000 }, - }, - "mistralai/mistral-small-3.1-24b-instruct": { - id: "mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Devstral 2 2512", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Codestral 2508", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/devstral-small-2505": { - id: "mistralai/devstral-small-2505", - name: "Devstral Small", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.12 }, - limit: { context: 128000, output: 128000 }, - }, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 96000, output: 8192 }, - }, - "mistralai/devstral-medium-2507": { - id: "mistralai/devstral-medium-2507", - name: "Devstral Medium", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/devstral-small-2507": { - id: "mistralai/devstral-small-2507", - name: "Devstral Small 1.1", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.2-11b-vision-instruct": { - id: "meta-llama/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "meta-llama/llama-3.2-3b-instruct:free": { - id: "meta-llama/llama-3.2-3b-instruct:free", - name: "Llama 3.2 3B Instruct (free)", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.3-70b-instruct:free": { - id: "meta-llama/llama-3.3-70b-instruct:free", - name: "Llama 3.3 70B Instruct (free)", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "x-ai/grok-4.20-multi-agent-beta": { - id: "x-ai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi - Agent Beta", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12 } }, - limit: { context: 2000000, output: 30000 }, - status: "beta", - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "Grok 3 Beta", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075, cache_write: 0.5 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-4.20-beta": { - id: "x-ai/grok-4.20-beta", - name: "Grok 4.20 Beta", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12 } }, - limit: { context: 2000000, output: 30000 }, - status: "beta", - }, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "Grok 3 Mini Beta", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075, cache_write: 0.5 }, - limit: { context: 131072, output: 8192 }, - }, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 131072, output: 8192 }, - }, - "tencent/hy3-preview": { - id: "tencent/hy3-preview", - name: "Hy3 preview", - family: "Hy", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.066, output: 0.26, cache_read: 0.029, cache_write: 0.029 }, - limit: { context: 256000, output: 64000 }, - }, - "poolside/laguna-m.1:free": { - id: "poolside/laguna-m.1:free", - name: "Laguna M.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "poolside/laguna-xs.2:free": { - id: "poolside/laguna-xs.2:free", - name: "Laguna XS.2", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Intellect 3", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", - name: "Nemotron 3 Nano Omni (free)", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-28", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 65536 }, - }, - "nvidia/nemotron-3-nano-30b-a3b:free": { - id: "nvidia/nemotron-3-nano-30b-a3b:free", - name: "Nemotron 3 Nano 30B A3B (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-12-14", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "nvidia/nemotron-nano-9b-v2:free": { - id: "nvidia/nemotron-nano-9b-v2:free", - name: "Nemotron Nano 9B V2 (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-09-05", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "Nemotron 3 Super (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/nemotron-nano-12b-v2-vl:free": { - id: "nvidia/nemotron-nano-12b-v2-vl:free", - name: "Nemotron Nano 12B 2 VL (free)", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-10-28", - last_updated: "2026-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "inception/mercury-edit-2": { - id: "inception/mercury-edit-2", - name: "Mercury Edit 2", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 8192 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-04", - last_updated: "2026-03-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-oss-120b:exacto": { - id: "openai/gpt-oss-120b:exacto", - name: "GPT OSS 120B (exacto)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.24 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-20b:free": { - id: "openai/gpt-oss-20b:free", - name: "gpt-oss-20b (free)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4 Mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "GPT-5 Image", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-10-14", - last_updated: "2025-10-14", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 5, output: 10, cache_read: 1.25 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "openai/gpt-oss-120b:free": { - id: "openai/gpt-oss-120b:free", - name: "gpt-oss-120b (free)", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.28 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "GLM 4.5 Air (free)", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131000 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-4.6:exacto": { - id: "z-ai/glm-4.6:exacto", - name: "GLM 4.6 (exacto)", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.9, cache_read: 0.11 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 128000, output: 96000 }, - }, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.96, output: 3.2, cache_read: 0.192, cache_write: 0 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "GLM 4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 65535 }, - }, - "sourceful/riverflow-v2-standard-preview": { - id: "sourceful/riverflow-v2-standard-preview", - name: "Riverflow V2 Standard Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "sourceful/riverflow-v2-fast-preview": { - id: "sourceful/riverflow-v2-fast-preview", - name: "Riverflow V2 Fast Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "sourceful/riverflow-v2-max-preview": { - id: "sourceful/riverflow-v2-max-preview", - name: "Riverflow V2 Max Preview", - family: "sourceful", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-08", - last_updated: "2026-01-28", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 8192 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2025-10-23", - last_updated: "2025-10-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.15, cache_read: 0.28, cache_write: 1.15 }, - limit: { context: 196600, output: 118000 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax-01", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000000, output: 1000000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5:free": { - id: "minimax/minimax-m2.5:free", - name: "MiniMax M2.5 (free)", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 3.25, cache_read: 0.13, cache_write: 0.8125 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-02-01", - last_updated: "2025-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.26, output: 0.78, cache_read: 0.052, cache_write: 0.325 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-25", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.065, output: 0.26 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.325, output: 1.95, cache_read: 0.0325, cache_write: 0.40625 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6, cache_read: 0.156, cache_write: 0.975 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen3-coder:exacto": { - id: "qwen/qwen3-coder:exacto", - name: "Qwen3 Coder (exacto)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 1.53 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen/qwen-3.6-27b": { - id: "qwen/qwen-3.6-27b", - name: "Qwen3.6 27B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.195, output: 1.56 }, - limit: { context: 262144, output: 81920 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.078, output: 0.312 }, - limit: { context: 262144, output: 81920 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5, cache_read: 0.039, cache_write: 0.24375 }, - limit: { context: 128000, output: 66536 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen-2.5-coder-32b-instruct": { - id: "qwen/qwen-2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 65536 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen3.5 Plus 2026-02-15", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-07-25": { - id: "qwen/qwen3-235b-a22b-07-25", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.85 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-4b-it:free": { - id: "google/gemma-3-4b-it:free", - name: "Gemma 3 4B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash-001": { - id: "google/gemini-2.0-flash-001", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Gemma 3n 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.25, - output: 1.5, - reasoning: 1.5, - cache_read: 0.025, - cache_write: 0.083, - input_audio: 0.5, - output_audio: 0.5, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3n-e4b-it:free": { - id: "google/gemma-3n-e4b-it:free", - name: "Gemma 3n 4B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12 }, - limit: { context: 1050000, output: 66000 }, - }, - "google/gemma-3n-e2b-it:free": { - id: "google/gemma-3n-e2b-it:free", - name: "Gemma 3n 2B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "google/gemma-2-9b-it": { - id: "google/gemma-2-9b-it", - name: "Gemma 2 9B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-28", - last_updated: "2024-06-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 8192, output: 8192 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "google/gemini-2.5-pro-preview-06-05": { - id: "google/gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemma-3-27b-it:free": { - id: "google/gemma-3-27b-it:free", - name: "Gemma 3 27B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-17", - last_updated: "2025-07-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-flash-image-preview": { - id: "google/gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image Preview (Nano Banana 2)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 65536, output: 65536 }, - }, - "google/gemma-4-31b-it:free": { - id: "google/gemma-4-31b-it:free", - name: "Gemma 4 31B (free)", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "google/gemma-3-12b-it:free": { - id: "google/gemma-3-12b-it:free", - name: "Gemma 3 12B (free)", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "google/gemma-3-4b-it": { - id: "google/gemma-3-4b-it", - name: "Gemma 3 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01703, output: 0.06815 }, - limit: { context: 96000, output: 96000 }, - }, - "google/gemini-2.5-flash-preview-09-2025": { - id: "google/gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.031 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 96000, output: 96000 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "google/gemma-4-26b-a4b-it:free": { - id: "google/gemma-4-26b-a4b-it:free", - name: "Gemma 4 26B A4B (free)", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-0905:exacto": { - id: "moonshotai/kimi-k2-0905:exacto", - name: "Kimi K2 Instruct 0905 (exacto)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 128000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-30", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1048576, output: 393216 }, - }, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1048576, output: 393216 }, - }, - "x-ai/grok-4.3": { - id: "x-ai/grok-4.3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-05-01", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 2.5, - cache_read: 0.2, - context_over_200k: { input: 2.5, output: 5, cache_read: 0.4 }, - }, - limit: { context: 1000000, output: 1000000 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "Xiaomi: MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "Xiaomi: MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 131072 }, - }, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "Xiaomi: MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "Xiaomi: MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - "fireworks-ai": { - id: "fireworks-ai", - env: ["FIREWORKS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.fireworks.ai/inference/v1/", - name: "Fireworks AI", - doc: "https://fireworks.ai/docs/", - models: { - "accounts/fireworks/models/glm-5p1": { - id: "accounts/fireworks/models/glm-5p1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202800, output: 131072 }, - }, - "accounts/fireworks/models/deepseek-v3p2": { - id: "accounts/fireworks/models/deepseek-v3p2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-09", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.28 }, - limit: { context: 160000, output: 160000 }, - }, - "accounts/fireworks/models/minimax-m2p5": { - id: "accounts/fireworks/models/minimax-m2p5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 196608, output: 196608 }, - }, - "accounts/fireworks/models/glm-4p5-air": { - id: "accounts/fireworks/models/glm-4p5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 131072, output: 131072 }, - }, - "accounts/fireworks/models/glm-5": { - id: "accounts/fireworks/models/glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.5 }, - limit: { context: 202752, output: 131072 }, - }, - "accounts/fireworks/models/deepseek-v3p1": { - id: "accounts/fireworks/models/deepseek-v3p1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 163840, output: 163840 }, - }, - "accounts/fireworks/models/kimi-k2p6": { - id: "accounts/fireworks/models/kimi-k2p6", - name: "Kimi K2.6", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262000, output: 262000 }, - }, - "accounts/fireworks/models/kimi-k2-instruct": { - id: "accounts/fireworks/models/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 128000, output: 16384 }, - }, - "accounts/fireworks/models/qwen3p6-plus": { - id: "accounts/fireworks/models/qwen3p6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-04", - last_updated: "2026-04-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.1 }, - limit: { context: 128000, output: 8192 }, - }, - "accounts/fireworks/models/minimax-m2p1": { - id: "accounts/fireworks/models/minimax-m2p1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 200000, output: 200000 }, - }, - "accounts/fireworks/models/minimax-m2p7": { - id: "accounts/fireworks/models/minimax-m2p7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-12", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 196608, output: 196608 }, - }, - "accounts/fireworks/models/glm-4p7": { - id: "accounts/fireworks/models/glm-4p7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.3 }, - limit: { context: 198000, output: 198000 }, - }, - "accounts/fireworks/models/glm-4p5": { - id: "accounts/fireworks/models/glm-4p5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 131072, output: 131072 }, - }, - "accounts/fireworks/models/kimi-k2p5": { - id: "accounts/fireworks/models/kimi-k2p5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - "accounts/fireworks/models/gpt-oss-20b": { - id: "accounts/fireworks/models/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "accounts/fireworks/models/gpt-oss-120b": { - id: "accounts/fireworks/models/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "accounts/fireworks/models/kimi-k2-thinking": { - id: "accounts/fireworks/models/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.3 }, - limit: { context: 256000, output: 256000 }, - }, - "accounts/fireworks/routers/kimi-k2p5-turbo": { - id: "accounts/fireworks/routers/kimi-k2p5-turbo", - name: "Kimi K2.5 Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "accounts/fireworks/models/deepseek-v4-pro": { - id: "accounts/fireworks/models/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.15 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - "kimi-for-coding": { - id: "kimi-for-coding", - env: ["KIMI_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.kimi.com/coding/v1", - name: "Kimi For Coding", - doc: "https://www.kimi.com/coding/docs/en/third-party-agents.html", - models: { - k2p6: { - id: "k2p6", - name: "Kimi K2.6", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04", - last_updated: "2026-04", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - k2p5: { - id: "k2p5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - }, - }, - moark: { - id: "moark", - env: ["MOARK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://moark.com/v1", - name: "Moark", - doc: "https://moark.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90", - models: { - "GLM-4.7": { - id: "GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.5, output: 14 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.1, output: 8.4 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "opencode-go": { - id: "opencode-go", - env: ["OPENCODE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://opencode.ai/zen/go/v1", - name: "OpenCode Go", - doc: "https://opencode.ai/docs/zen", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-k2.5", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 65536 }, - }, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - family: "mimo-v2.5-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 128000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 32768 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo V2 Omni", - family: "mimo-v2-omni", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 128000 }, - status: "deprecated", - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo V2.5", - family: "mimo-v2.5", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen3.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 262144, output: 65536 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 32768 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.0028 }, - limit: { context: 1000000, output: 384000 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 65536 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.0145 }, - limit: { context: 1000000, output: 384000 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 65536 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo V2 Pro", - family: "mimo-v2-pro", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 128000 }, - status: "deprecated", - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen3.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.2, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 262144, output: 65536 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - }, - }, - "io-net": { - id: "io-net", - env: ["IOINTELLIGENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.intelligence.io.solutions/api/v1", - name: "IO.NET", - doc: "https://io.net/docs/guides/intelligence/io-intelligence", - models: { - "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { - id: "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar", - name: "Qwen 3 Coder 480B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.95, cache_read: 0.11, cache_write: 0.44 }, - limit: { context: 106000, output: 4096 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen 3 Next 80B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-10", - last_updated: "2025-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.8, cache_read: 0.05, cache_write: 0.2 }, - limit: { context: 262144, output: 4096 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen 3 235B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6, cache_read: 0.055, cache_write: 0.22 }, - limit: { context: 262144, output: 4096 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen 2.5 VL 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22, cache_read: 0.025, cache_write: 0.1 }, - limit: { context: 32000, output: 4096 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-15", - last_updated: "2024-11-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.75, cache_read: 0.2, cache_write: 0.8 }, - limit: { context: 200000, output: 4096 }, - }, - "mistralai/Magistral-Small-2506": { - id: "mistralai/Magistral-Small-2506", - name: "Magistral Small 2506", - family: "magistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 0.25, cache_write: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Mistral-Large-Instruct-2411": { - id: "mistralai/Mistral-Large-Instruct-2411", - name: "Mistral Large Instruct 2411", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 1, cache_write: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04, cache_read: 0.01, cache_write: 0.04 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/Devstral-Small-2505": { - id: "mistralai/Devstral-Small-2505", - name: "Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.22, cache_read: 0.025, cache_write: 0.1 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.38, cache_read: 0.065, cache_write: 0.26 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B 128E Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6, cache_read: 0.075, cache_write: 0.3 }, - limit: { context: 430000, output: 4096 }, - }, - "meta-llama/Llama-3.2-90B-Vision-Instruct": { - id: "meta-llama/Llama-3.2-90B-Vision-Instruct", - name: "Llama 3.2 90B Vision Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.4, cache_read: 0.175, cache_write: 0.7 }, - limit: { context: 16000, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 8.75, cache_read: 1, cache_write: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT-OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14, cache_read: 0.015, cache_write: 0.06 }, - limit: { context: 64000, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.4, cache_read: 0.02, cache_write: 0.08 }, - limit: { context: 131072, output: 4096 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.25, cache_read: 0.275, cache_write: 1.1 }, - limit: { context: 32768, output: 4096 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-09-05", - last_updated: "2024-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39, output: 1.9, cache_read: 0.195, cache_write: 0.78 }, - limit: { context: 32768, output: 4096 }, - }, - }, - }, - "alibaba-cn": { - id: "alibaba-cn", - env: ["DASHSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://dashscope.aliyuncs.com/compatible-mode/v1", - name: "Alibaba (China)", - doc: "https://www.alibabacloud.com/help/en/model-studio/models", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen3 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 1.147, reasoning: 2.868 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen-plus-character": { - id: "qwen-plus-character", - name: "Qwen Plus Character", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287 }, - limit: { context: 32768, output: 4096 }, - }, - "qwen2-5-math-7b-instruct": { - id: "qwen2-5-math-7b-instruct", - name: "Qwen2.5-Math 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.287 }, - limit: { context: 4096, output: 3072 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Moonshot Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.411 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen-doc-turbo": { - id: "qwen-doc-turbo", - name: "Qwen Doc Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.087, output: 0.144 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-ocr": { - id: "qwen-vl-ocr", - name: "Qwen-VL OCR", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-28", - last_updated: "2025-04-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.717, output: 0.717 }, - limit: { context: 34096, output: 4096 }, - }, - "qwen-omni-turbo-realtime": { - id: "qwen-omni-turbo-realtime", - name: "Qwen-Omni Turbo Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-05-08", - last_updated: "2025-05-08", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.23, output: 0.918, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3-8b": { - id: "qwen3-8b", - name: "Qwen3 8B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.287, reasoning: 0.717 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 2.58, reasoning: 2.58 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-math-turbo": { - id: "qwen-math-turbo", - name: "Qwen Math Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 4096, output: 3072 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.574 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen-VL Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-08-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287 }, - limit: { context: 131072, output: 8192 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.86, output: 3.15 }, - limit: { context: 202752, output: 16384 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 1.147, reasoning: 2.868 }, - limit: { context: 131072, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.345, output: 1.377 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.115, output: 0.287, reasoning: 1.147 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen-Omni Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-19", - last_updated: "2025-03-26", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.058, output: 0.23, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.022, output: 0.216 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen2-5-vl-7b-instruct": { - id: "qwen2-5-vl-7b-instruct", - name: "Qwen2.5-VL 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.717 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen3.5-flash": { - id: "qwen3.5-flash", - name: "Qwen3.5 Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-23", - last_updated: "2026-02-23", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.172, output: 1.72, reasoning: 1.72 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.276, output: 1.651, cache_read: 0.028, cache_write: 0.344 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.861, output: 3.441 }, - limit: { context: 262144, output: 65536 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-14", - last_updated: "2026-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.87, output: 3.48, cache_read: 0.17 }, - limit: { context: 202752, output: 128000 }, - }, - "qwen3-omni-flash": { - id: "qwen3-omni-flash", - name: "Qwen3-Omni Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.058, output: 0.23, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 65536, output: 16384 }, - }, - "deepseek-v3-1": { - id: "deepseek-v3-1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen2-5-72b-instruct": { - id: "qwen2-5-72b-instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3-VL 235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.286705, output: 1.14682, reasoning: 2.867051 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-math-plus": { - id: "qwen-math-plus", - name: "Qwen Math Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-08-16", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 4096, output: 3072 }, - }, - "qwen2-5-coder-32b-instruct": { - id: "qwen2-5-coder-32b-instruct", - name: "Qwen2.5-Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-asr-flash": { - id: "qwen3-asr-flash", - name: "Qwen3-ASR Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.032, output: 0.032 }, - limit: { context: 53248, output: 4096 }, - }, - "qwen-deep-research": { - id: "qwen-deep-research", - name: "Qwen Deep Research", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 7.742, output: 23.367 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next 80B-A3B (Thinking)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 1.434 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-mt-plus": { - id: "qwen-mt-plus", - name: "Qwen-MT Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.259, output: 0.775 }, - limit: { context: 16384, output: 8192 }, - }, - "deepseek-r1-distill-qwen-32b": { - id: "deepseek-r1-distill-qwen-32b", - name: "DeepSeek R1 Distill Qwen 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen-VL Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.574 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.144, output: 0.574 }, - limit: { context: 1000000, output: 65536 }, - }, - "deepseek-r1-distill-qwen-7b": { - id: "deepseek-r1-distill-qwen-7b", - name: "DeepSeek R1 Distill Qwen 7B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.072, output: 0.144 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen2-5-7b-instruct": { - id: "qwen2-5-7b-instruct", - name: "Qwen2.5 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.072, output: 0.144 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-14b-instruct": { - id: "qwen2-5-14b-instruct", - name: "Qwen2.5 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.431 }, - limit: { context: 131072, output: 8192 }, - }, - "tongyi-intent-detect-v3": { - id: "tongyi-intent-detect-v3", - name: "Tongyi Intent Detect V3", - family: "yi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.058, output: 0.144 }, - limit: { context: 8192, output: 1024 }, - }, - "qwq-32b": { - id: "qwq-32b", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshot-kimi-k2-instruct": { - id: "moonshot-kimi-k2-instruct", - name: "Moonshot Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-32b-instruct": { - id: "qwen2-5-32b-instruct", - name: "Qwen2.5 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.287, output: 0.861 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next 80B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.574 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-omni-flash-realtime": { - id: "qwen3-omni-flash-realtime", - name: "Qwen3-Omni Flash Realtime", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image", "audio"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.23, output: 0.918, input_audio: 3.584, output_audio: 7.168 }, - limit: { context: 65536, output: 16384 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Moonshot Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.929, output: 3.858 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen3-vl-30b-a3b": { - id: "qwen3-vl-30b-a3b", - name: "Qwen3-VL 30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.108, output: 0.431, reasoning: 1.076 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.143353, output: 1.433525, reasoning: 4.300576 }, - limit: { context: 262144, output: 32768 }, - }, - "deepseek-v3-2-exp": { - id: "deepseek-v3-2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 0.431 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3-Coder 480B-A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.861, output: 3.441 }, - limit: { context: 262144, output: 65536 }, - }, - "deepseek-r1-distill-qwen-1-5b": { - id: "deepseek-r1-distill-qwen-1-5b", - name: "DeepSeek R1 Distill Qwen 1.5B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.216, output: 0.861 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen2-5-coder-7b-instruct": { - id: "qwen2-5-coder-7b-instruct", - name: "Qwen2.5-Coder 7B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.287 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11-01", - last_updated: "2025-07-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.044, output: 0.087, reasoning: 0.431 }, - limit: { context: 1000000, output: 16384 }, - }, - "qwen-mt-turbo": { - id: "qwen-mt-turbo", - name: "Qwen-MT Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.101, output: 0.28 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen2-5-math-72b-instruct": { - id: "qwen2-5-math-72b-instruct", - name: "Qwen2.5-Math 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 1.721 }, - limit: { context: 4096, output: 3072 }, - }, - "qwen3.6-max-preview": { - id: "qwen3.6-max-preview", - name: "Qwen3.6 Max Preview", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.32, output: 7.9, cache_read: 0.132 }, - limit: { context: 245800, output: 65536 }, - }, - "qwen2-5-omni-7b": { - id: "qwen2-5-omni-7b", - name: "Qwen2.5-Omni 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.087, output: 0.345, input_audio: 5.448 }, - limit: { context: 32768, output: 2048 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.573, output: 3.44, reasoning: 3.44 }, - limit: { context: 1000000, output: 65536 }, - }, - "deepseek-r1-distill-qwen-14b": { - id: "deepseek-r1-distill-qwen-14b", - name: "DeepSeek R1 Distill Qwen 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.144, output: 0.431 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5-VL 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.294, output: 6.881 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.287, output: 1.147 }, - limit: { context: 65536, output: 8192 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 131072, output: 16384 }, - }, - "qvq-max": { - id: "qvq-max", - name: "QVQ Max", - family: "qvq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.147, output: 4.588 }, - limit: { context: 131072, output: 8192 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Moonshot Kimi K2 Thinking", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.574, output: 2.294 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen3-14b": { - id: "qwen3-14b", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.144, output: 0.574, reasoning: 1.434 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-r1-distill-llama-8b": { - id: "deepseek-r1-distill-llama-8b", - name: "DeepSeek R1 Distill Llama 8B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen-long": { - id: "qwen-long", - name: "Qwen Long", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.072, output: 0.287 }, - limit: { context: 10000000, output: 8192 }, - }, - "kimi/kimi-k2.5": { - id: "kimi/kimi-k2.5", - name: "kimi/kimi-k2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMax/MiniMax-M2.7": { - id: "MiniMax/MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "siliconflow/deepseek-v3-0324": { - id: "siliconflow/deepseek-v3-0324", - name: "siliconflow/deepseek-v3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 163840, output: 163840 }, - }, - "siliconflow/deepseek-v3.2": { - id: "siliconflow/deepseek-v3.2", - name: "siliconflow/deepseek-v3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 163840, output: 65536 }, - }, - "siliconflow/deepseek-r1-0528": { - id: "siliconflow/deepseek-r1-0528", - name: "siliconflow/deepseek-r1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 163840, output: 32768 }, - }, - "siliconflow/deepseek-v3.1-terminus": { - id: "siliconflow/deepseek-v3.1-terminus", - name: "siliconflow/deepseek-v3.1-terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 163840, output: 65536 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1048576, output: 65536 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - firepass: { - id: "firepass", - env: ["FIREPASS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.fireworks.ai/inference/v1/", - name: "Fireworks (Firepass)", - doc: "https://docs.fireworks.ai/firepass", - models: { - "accounts/fireworks/routers/kimi-k2p6-turbo": { - id: "accounts/fireworks/routers/kimi-k2p6-turbo", - name: "Kimi K2.6 Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - "minimax-cn-coding-plan": { - id: "minimax-cn-coding-plan", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimaxi.com/anthropic/v1", - name: "MiniMax Coding Plan (minimaxi.com)", - doc: "https://platform.minimaxi.com/docs/coding-plan/intro", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - jiekou: { - id: "jiekou", - env: ["JIEKOU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.jiekou.ai/openai", - name: "Jiekou.AI", - doc: "https://docs.jiekou.ai/docs/support/quickstart?utm_source=github_models.dev", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "gpt-5.1-codex-max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "grok-4-1-fast-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "claude-opus-4-5-20251101", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.5, output: 22.5 }, - limit: { context: 200000, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "gemini-2.5-flash-lite-preview-09-2025", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "gpt-5.2-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 18.9, output: 151.2 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.225, output: 1.8 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "gpt-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045, output: 0.36 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "gemini-3-pro-preview", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 10.8 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "gemini-2.5-flash-preview-05-20", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.135, output: 3.15 }, - limit: { context: 1048576, output: 200000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "claude-sonnet-4-5-20250929", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 1048576, output: 65535 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "grok-4-1-fast-non-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "gpt-5.2", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.575, output: 12.6 }, - limit: { context: 400000, output: 128000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "gemini-2.5-pro-preview-06-05", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 1048576, output: 200000 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "gemini-2.5-flash-lite-preview-06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "video", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "gpt-5.2-codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 2.25 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "gpt-5.1-codex-mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.225, output: 1.8 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "grok-code-fast-1", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 1.35 }, - limit: { context: 256000, output: 256000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "gpt-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "grok-4-fast-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 131072, output: 131072 }, - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "grok-4-0709", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 256000, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "gpt-5-codex", - family: "gpt-codex", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "claude-opus-4-1-20250805", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 67.5 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "claude-haiku-4-5-20251001", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 4.5 }, - limit: { context: 20000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "claude-sonnet-4-20250514", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.7, output: 13.5 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "claude-opus-4-6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - o3: { - id: "o3", - name: "o3", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "gpt-5-pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 108 }, - limit: { context: 400000, output: 272000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "gemini-2.5-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 1048576, output: 65535 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "gpt-5-chat-latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "claude-opus-4-20250514", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 13.5, output: 67.5 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "gpt-5.1-codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.125, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "grok-4-fast-non-reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.45 }, - limit: { context: 2000000, output: 2000000 }, - }, - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.14 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 163840, output: 32768 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 163840, output: 32768 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5v": { - id: "zai-org/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 65536, output: 16384 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 128000 }, - }, - "minimaxai/minimax-m1-80k": { - id: "minimaxai/minimax-m1-80k", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "xiaomimimo/mimo-v2-flash": { - id: "xiaomimimo/mimo-v2-flash", - name: "XiaomiMiMo/MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "ERNIE 4.5 VL 424B A47B", - family: "ernie", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/ernie-4.5-300b-a47b-paddle": { - id: "baidu/ernie-4.5-300b-a47b-paddle", - name: "ERNIE 4.5 300B A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3-235b-a22b-instruct-2507": { - id: "qwen/qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.8 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen/qwen3-32b-fp8": { - id: "qwen/qwen3-32b-fp8", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22b Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 65536, output: 65536 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 65536, output: 65536 }, - }, - "qwen/qwen3-30b-a3b-fp8": { - id: "qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "qwen/qwen3-coder-next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 1.2 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-fp8": { - id: "qwen/qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 40960, output: 20000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.3 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - bailing: { - id: "bailing", - env: ["BAILING_API_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.tbox.cn/api/llm/v1/chat/completions", - name: "Bailing", - doc: "https://alipaytbox.yuque.com/sxs0ba/ling/intro", - models: { - "Ring-1T": { - id: "Ring-1T", - name: "Ring-1T", - family: "ring", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.29 }, - limit: { context: 128000, output: 32000 }, - }, - "Ling-1T": { - id: "Ling-1T", - name: "Ling-1T", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.29 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - iflowcn: { - id: "iflowcn", - env: ["IFLOW_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://apis.iflow.cn/v1", - name: "iFlow", - doc: "https://platform.iflow.cn/en/docs", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3-Coder-Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3-Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-235b-a22b-instruct": { - id: "qwen3-235b-a22b-instruct", - name: "Qwen3-235B-A22B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3-235B-A22B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "kimi-k2-0905": { - id: "kimi-k2-0905", - name: "Kimi-K2-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL-Plus", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2-Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 64000 }, - }, - "qwen3-235b": { - id: "qwen3-235b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi-K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 64000 }, - }, - "qwen3-max-preview": { - id: "qwen3-max-preview", - name: "Qwen3-Max-Preview", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 32000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - v0: { - id: "v0", - env: ["V0_API_KEY"], - npm: "@ai-sdk/vercel", - name: "v0", - doc: "https://sdk.vercel.ai/providers/ai-sdk-providers/vercel", - models: { - "v0-1.5-lg": { - id: "v0-1.5-lg", - name: "v0-1.5-lg", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75 }, - limit: { context: 512000, output: 32000 }, - }, - "v0-1.0-md": { - id: "v0-1.0-md", - name: "v0-1.0-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "v0-1.5-md": { - id: "v0-1.5-md", - name: "v0-1.5-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - }, - }, - huggingface: { - id: "huggingface", - env: ["HF_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://router.huggingface.co/v1", - name: "Hugging Face", - doc: "https://huggingface.co/docs/inference-providers", - models: { - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 32768 }, - }, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3-Coder-Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen 3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 4096 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Embedding-4B": { - id: "Qwen/Qwen3-Embedding-4B", - name: "Qwen 3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 2048 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 262144, output: 66536 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202752, output: 131072 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 4096 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 5 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.4 }, - limit: { context: 163840, output: 65536 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi-K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi-K2-Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-01", - last_updated: "2026-01-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1048576, output: 393216 }, - }, - }, - }, - zenmux: { - id: "zenmux", - env: ["ZENMUX_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://zenmux.ai/api/v1", - name: "ZenMux", - doc: "https://docs.zenmux.ai", - models: { - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek-V3.2 (Non-thinking Mode)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 128000, output: 64000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek-V3.2-Exp", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 0.33 }, - limit: { context: 163000, output: 64000 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-05", - last_updated: "2025-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.43 }, - limit: { context: 128000, output: 64000 }, - }, - "inclusionai/ring-1t": { - id: "inclusionai/ring-1t", - name: "Ring-1T", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-12", - last_updated: "2025-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 2.24, cache_read: 0.11 }, - limit: { context: 128000, output: 64000 }, - }, - "inclusionai/ling-1t": { - id: "inclusionai/ling-1t", - name: "Ling-1T", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-09", - last_updated: "2025-10-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 2.24, cache_read: 0.11 }, - limit: { context: 128000, output: 64000 }, - }, - "stepfun/step-3.5-flash-free": { - id: "stepfun/step-3.5-flash-free", - name: "Step 3.5 Flash (Free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 256000, output: 64000 }, - }, - "stepfun/step-3": { - id: "stepfun/step-3", - name: "Step-3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 65536, output: 64000 }, - }, - "kuaishou/kat-coder-pro-v2": { - id: "kuaishou/kat-coder-pro-v2", - name: "KAT-Coder-Pro-V2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 80000 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "Grok 4 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "Grok Code Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-4.1-fast-non-reasoning": { - id: "x-ai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non Reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "Grok 4.1 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 64000 }, - }, - "x-ai/grok-4.2-fast": { - id: "x-ai/grok-4.2-fast", - name: "Grok 4.2 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 9 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-4.2-fast-non-reasoning": { - id: "x-ai/grok-4.2-fast-non-reasoning", - name: "Grok 4.2 Fast Non Reasoning", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 9 }, - limit: { context: 2000000, output: 30000 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16380 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01-01", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.17 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["pdf", "image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 128000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4 Nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25 }, - limit: { context: 400000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-01-15", - last_updated: "2026-01-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.17 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 45, output: 225 }, - limit: { context: 1050000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.75, output: 18.75 }, - limit: { context: 1050000, output: 128000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12 }, - limit: { context: 400000, output: 64000 }, - provider: { npm: "@ai-sdk/openai", api: "https://zenmux.ai/api/v1" }, - }, - "z-ai/glm-4.7-flash-free": { - id: "z-ai/glm-4.7-flash-free", - name: "GLM 4.7 Flash (Free)", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "GLM 5V Turbo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.726, output: 3.1946, cache_read: 0.1743 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.14, cache_read: 0.06 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 2.6, cache_read: 0.14 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.7-flashx": { - id: "z-ai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.42, cache_read: 0.01 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.6v-flash-free": { - id: "z-ai/glm-4.6v-flash-free", - name: "GLM 4.6V Flash (Free)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8781, output: 3.5126, cache_read: 0.1903 }, - limit: { context: 200000, output: 131072 }, - }, - "z-ai/glm-4.6v-flash": { - id: "z-ai/glm-4.6v-flash", - name: "GLM 4.6V FlashX", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.21, cache_read: 0.0043 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.54, cache_read: 0.07 }, - limit: { context: 128000, output: 64000 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.56, cache_read: 0.02 }, - limit: { context: 128000, output: 64000 }, - }, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "GLM 5 Turbo", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.88, output: 3.48 }, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "GLM 4.6", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.54, cache_read: 0.07 }, - limit: { context: 200000, output: 64000 }, - }, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "GLM 4.6V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.42, cache_read: 0.03 }, - limit: { context: 200000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-code": { - id: "volcengine/doubao-seed-2.0-code", - name: "Doubao Seed 2.0 Code", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 4.48 }, - limit: { context: 256000, output: 32000 }, - }, - "volcengine/doubao-seed-code": { - id: "volcengine/doubao-seed-code", - name: "Doubao-Seed-Code", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-11", - last_updated: "2025-11-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.17, output: 1.12, cache_read: 0.03 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-mini": { - id: "volcengine/doubao-seed-2.0-mini", - name: "Doubao-Seed-2.0-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.28, cache_read: 0.01, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-lite": { - id: "volcengine/doubao-seed-2.0-lite", - name: "Doubao-Seed-2.0-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.51, cache_read: 0.02, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-1.8": { - id: "volcengine/doubao-seed-1.8", - name: "Doubao-Seed-1.8", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.28, cache_read: 0.02, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "volcengine/doubao-seed-2.0-pro": { - id: "volcengine/doubao-seed-2.0-pro", - name: "Doubao-Seed-2.0-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-14", - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 2.24, cache_read: 0.09, cache_write: 0.0024 }, - limit: { context: 256000, output: 64000 }, - }, - "baidu/ernie-5.0-thinking-preview": { - id: "baidu/ernie-5.0-thinking-preview", - name: "ERNIE 5.0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.84, output: 3.37 }, - limit: { context: 128000, output: 64000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3055, output: 1.2219 }, - limit: { context: 204800, output: 131070 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 highspeed", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.611, output: 2.4439 }, - limit: { context: 204800, output: 131070 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.5-lightning": { - id: "minimax/minimax-m2.5-lightning", - name: "MiniMax M2.5 highspeed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4.8, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen3-Coder-Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen/qwen3.5-flash": { - id: "qwen/qwen3.5-flash", - name: "Qwen3.5 Flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1020000, output: 1020000 }, - }, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen3.6-Plus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - cache_write: 0.625, - context_over_200k: { input: 2, output: 6, cache_read: 0.2, cache_write: 2.5 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3-Max-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen/qwen3.5-plus": { - id: "qwen/qwen3.5-plus", - name: "Qwen3.5 Plus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-03-20", - last_updated: "2026-03-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4.8 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 1050000, output: 65530 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-19", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "pdf", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, cache_write: 4.5 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "pdf", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["pdf", "image", "text", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31, cache_write: 4.5 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["pdf", "image", "text", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.07, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["pdf", "image", "text", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03, cache_write: 1 }, - limit: { context: 1048000, output: 64000 }, - }, - "sapiens-ai/agnes-1.5-lite": { - id: "sapiens-ai/agnes-1.5-lite", - name: "Agnes 1.5 Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-26", - last_updated: "2026-03-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "sapiens-ai/agnes-1.5-pro": { - id: "sapiens-ai/agnes-1.5-pro", - name: "Agnes 1.5 Pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-21", - last_updated: "2026-03-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.8 }, - limit: { context: 256000, output: 256000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.58, output: 3.02, cache_read: 0.1 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262140, output: 262140 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262000, output: 64000 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["pdf", "image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["image", "text", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2024-11-04", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic", api: "https://zenmux.ai/api/anthropic/v1" }, - }, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - }, - "tencent/hy3-preview": { - id: "tencent/hy3-preview", - name: "Hy3 preview", - family: "Hy", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.172, output: 0.572, cache_read: 0.058, cache_write: 0 }, - limit: { context: 256000, output: 64000 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 12.5, output: 75, cache_read: 1.25 }, - provider: { body: { service_tier: "priority" } }, - }, - }, - }, - }, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "MiMo V2 Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 265000, output: 265000 }, - }, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1000000, output: 256000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - upstage: { - id: "upstage", - env: ["UPSTAGE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.upstage.ai/v1/solar", - name: "Upstage", - doc: "https://developers.upstage.ai/docs/apis/chat", - models: { - "solar-pro2": { - id: "solar-pro2", - name: "solar-pro2", - family: "solar-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 65536, output: 8192 }, - }, - "solar-mini": { - id: "solar-mini", - name: "solar-mini", - family: "solar-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-06-12", - last_updated: "2025-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 4096 }, - }, - "solar-pro3": { - id: "solar-pro3", - name: "solar-pro3", - family: "solar-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 131072, output: 8192 }, - }, - }, - }, - "novita-ai": { - id: "novita-ai", - env: ["NOVITA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.novita.ai/openai", - name: "NovitaAI", - doc: "https://novita.ai/docs/guides/introduction", - models: { - "deepseek/deepseek-r1-turbo": { - id: "deepseek/deepseek-r1-turbo", - name: "DeepSeek R1 (Turbo)\t", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.12, cache_read: 0.135 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-ocr-2": { - id: "deepseek/deepseek-ocr-2", - name: "deepseek/deepseek-ocr-2", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-ocr": { - id: "deepseek/deepseek-ocr", - name: "DeepSeek-OCR", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-24", - last_updated: "2025-10-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill LLama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 8192, output: 8192 }, - }, - "deepseek/deepseek-prover-v2-671b": { - id: "deepseek/deepseek-prover-v2-671b", - name: "Deepseek Prover V2 671B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 160000, output: 160000 }, - }, - "deepseek/deepseek-r1-0528-qwen3-8b": { - id: "deepseek/deepseek-r1-0528-qwen3-8b", - name: "DeepSeek R1 0528 Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-05-29", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.09 }, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-r1-distill-qwen-32b": { - id: "deepseek/deepseek-r1-distill-qwen-32b", - name: "DeepSeek R1 Distill Qwen 32B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 64000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "Deepseek V3.2 Exp", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1, cache_read: 0.135 }, - limit: { context: 131072, output: 32768 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "Deepseek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.269, output: 0.4, cache_read: 0.1345 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3-turbo": { - id: "deepseek/deepseek-v3-turbo", - name: "DeepSeek V3 (Turbo)\t", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.3 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-r1-distill-qwen-14b": { - id: "deepseek/deepseek-r1-distill-qwen-14b", - name: "DeepSeek R1 Distill Qwen 14B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 16384 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5, cache_read: 0.35 }, - limit: { context: 163840, output: 32768 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "Deepseek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1, cache_read: 0.135 }, - limit: { context: 131072, output: 32768 }, - }, - "inclusionai/ling-2.6-1t": { - id: "inclusionai/ling-2.6-1t", - name: "Ling-2.6-1T", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "paddlepaddle/paddleocr-vl": { - id: "paddlepaddle/paddleocr-vl", - name: "PaddleOCR-VL", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 16384, output: 16384 }, - }, - "nousresearch/hermes-2-pro-llama-3-8b": { - id: "nousresearch/hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - }, - "zai-org/glm-4.7": { - id: "zai-org/glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-5": { - id: "zai-org/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202800, output: 131072 }, - }, - "zai-org/glm-5.1": { - id: "zai-org/glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.5": { - id: "zai-org/glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5-air": { - id: "zai-org/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-10-13", - last_updated: "2025-10-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.85 }, - limit: { context: 131072, output: 98304 }, - }, - "zai-org/glm-4.5v": { - id: "zai-org/glm-4.5v", - name: "GLM 4.5V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.11 }, - limit: { context: 65536, output: 16384 }, - }, - "zai-org/glm-4.6": { - id: "zai-org/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2, cache_read: 0.11 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/glm-4.6v": { - id: "zai-org/glm-4.6v", - name: "GLM 4.6V", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9, cache_read: 0.055 }, - limit: { context: 131072, output: 32768 }, - }, - "zai-org/glm-4.7-flash": { - id: "zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01 }, - limit: { context: 200000, output: 128000 }, - }, - "zai-org/autoglm-phone-9b-multilingual": { - id: "zai-org/autoglm-phone-9b-multilingual", - name: "AutoGLM-Phone-9B-Multilingual", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-12-10", - last_updated: "2025-12-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.035, output: 0.138 }, - limit: { context: 65536, output: 65536 }, - }, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-07-30", - last_updated: "2024-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.17 }, - limit: { context: 60288, output: 16000 }, - }, - "baichuan/baichuan-m2-32b": { - id: "baichuan/baichuan-m2-32b", - name: "baichuan-m2-32b", - family: "baichuan", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2024-12", - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-06", - last_updated: "2025-04-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.59 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-07", - last_updated: "2024-12-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.135, output: 0.4 }, - limit: { context: 131072, output: 120000 }, - }, - "meta-llama/llama-3.2-3b-instruct": { - id: "meta-llama/llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 32768, output: 32000 }, - }, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 8192, output: 8192 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 16384, output: 16384 }, - }, - "meta-llama/llama-3-70b-instruct": { - id: "meta-llama/llama-3-70b-instruct", - name: "Llama3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "meta-llama/llama-4-maverick-17b-128e-instruct-fp8": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-06", - last_updated: "2025-04-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 1048576, output: 8192 }, - }, - "gryphe/mythomax-l2-13b": { - id: "gryphe/mythomax-l2-13b", - name: "Mythomax L2 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.09 }, - limit: { context: 4096, output: 3200 }, - }, - "sao10k/l31-70b-euryale-v2.2": { - id: "sao10k/l31-70b-euryale-v2.2", - name: "L31 70B Euryale V2.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3-70b-euryale-v2.1": { - id: "sao10k/l3-70b-euryale-v2.1", - name: "L3 70B Euryale V2.1\t", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-06-18", - last_updated: "2024-06-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/L3-8B-Stheno-v3.2": { - id: "sao10k/L3-8B-Stheno-v3.2", - name: "L3 8B Stheno V3.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-29", - last_updated: "2024-11-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 8192, output: 32000 }, - }, - "sao10k/l3-8b-lunaris": { - id: "sao10k/l3-8b-lunaris", - name: "Sao10k L3 8B Lunaris\t", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-11-28", - last_updated: "2024-11-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 8192, output: 8192 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "Wizardlm 2 8x22B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-24", - last_updated: "2024-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 0.62 }, - limit: { context: 65535, output: 8000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: GPT OSS 20B", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 131072, output: 32768 }, - }, - "minimaxai/minimax-m1-80k": { - id: "minimaxai/minimax-m1-80k", - name: "MiniMax M1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "xiaomimimo/mimo-v2-flash": { - id: "xiaomimimo/mimo-v2-flash", - name: "XiaomiMiMo/MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.3 }, - limit: { context: 262144, output: 32000 }, - }, - "baidu/ernie-4.5-vl-28b-a3b-thinking": { - id: "baidu/ernie-4.5-vl-28b-a3b-thinking", - name: "ERNIE-4.5-VL-28B-A3B-Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 0.39 }, - limit: { context: 131072, output: 65536 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "ERNIE 4.5 VL 424B A47B", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/ernie-4.5-21B-a3b": { - id: "baidu/ernie-4.5-21B-a3b", - name: "ERNIE 4.5 21B A3B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 120000, output: 8000 }, - }, - "baidu/ernie-4.5-300b-a47b-paddle": { - id: "baidu/ernie-4.5-300b-a47b-paddle", - name: "ERNIE 4.5 300B A47B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "baidu/ernie-4.5-21B-a3b-thinking": { - id: "baidu/ernie-4.5-21B-a3b-thinking", - name: "ERNIE-4.5-21B-A3B-Thinking", - family: "ernie", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131072, output: 65536 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "ERNIE 4.5 VL 28B A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 5.6 }, - limit: { context: 30000, output: 8000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 Highspeed", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4, cache_read: 0.03 }, - limit: { context: 204800, output: 131100 }, - }, - "qwen/qwen2.5-7b-instruct": { - id: "qwen/qwen2.5-7b-instruct", - name: "Qwen2.5 7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.07 }, - limit: { context: 32000, output: 32000 }, - }, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5-122B-A10B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 3.2 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3.6-27b": { - id: "qwen/qwen3.6-27b", - name: "Qwen3.6-27B", - family: "qwen3.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen3.5-27B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.4 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-instruct-2507": { - id: "qwen/qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.58 }, - limit: { context: 131072, output: 16384 }, - }, - "qwen/qwen3-omni-30b-a3b-instruct": { - id: "qwen/qwen3-omni-30b-a3b-instruct", - name: "Qwen3 Omni 30B A3B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "video", "audio", "image"], output: ["text", "audio"] }, - open_weights: true, - cost: { input: 0.25, output: 0.97, input_audio: 2.2, output_audio: 1.788 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 64000 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen2.5 VL 72B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.98, output: 3.95 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "qwen/qwen3-vl-30b-a3b-thinking", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-omni-30b-a3b-thinking": { - id: "qwen/qwen3-omni-30b-a3b-thinking", - name: "Qwen3 Omni 30B A3B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "audio", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.97, input_audio: 2.2, output_audio: 1.788 }, - limit: { context: 65536, output: 16384 }, - }, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "qwen/qwen3-vl-8b-instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-17", - last_updated: "2025-10-17", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.11, output: 8.45 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-32b-fp8": { - id: "qwen/qwen3-32b-fp8", - name: "Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-4b-fp8": { - id: "qwen/qwen3-4b-fp8", - name: "Qwen3 4B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 128000, output: 20000 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22b Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 3 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen-mt-plus": { - id: "qwen/qwen-mt-plus", - name: "Qwen MT Plus", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-03", - last_updated: "2025-09-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.75 }, - limit: { context: 16384, output: 8192 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-fp8": { - id: "qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.3 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "qwen/qwen3-vl-30b-a3b-instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text", "video", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30b A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-09", - last_updated: "2025-10-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "qwen/qwen3-235b-a22b-fp8": { - id: "qwen/qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 40960, output: 20000 }, - }, - "qwen/qwen3-8b-fp8": { - id: "qwen/qwen3-8b-fp8", - name: "Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.035, output: 0.138 }, - limit: { context: 128000, output: 20000 }, - }, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen 2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-15", - last_updated: "2024-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 0.4 }, - limit: { context: 32000, output: 8192 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen3.5-35B-A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 65536 }, - }, - "kwaipilot/kat-coder-pro": { - id: "kwaipilot/kat-coder-pro", - name: "Kat Coder Pro", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-05", - last_updated: "2026-01-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 128000 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-25", - last_updated: "2025-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.119, output: 0.2 }, - limit: { context: 98304, output: 16384 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.57, output: 2.3 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1048576, output: 393216 }, - }, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.69, output: 3.38, cache_read: 0.13 }, - limit: { context: 1048576, output: 393216 }, - }, - }, - }, - "xiaomi-token-plan-cn": { - id: "xiaomi-token-plan-cn", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-cn.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (China)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 16384 }, - }, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - wandb: { - id: "wandb", - env: ["WANDB_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.inference.wandb.ai/v1", - name: "Weights & Biases", - doc: "https://docs.wandb.ai/guides/integrations/inference/", - models: { - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5-FP8": { - id: "zai-org/GLM-5-FP8", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 200000, output: 200000 }, - }, - "meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 64000, output: 64000 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 128000 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 128000 }, - }, - "meta-llama/Llama-3.1-70B-Instruct": { - id: "meta-llama/Llama-3.1-70B-Instruct", - name: "Llama 3.1 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 128000, output: 128000 }, - }, - "OpenPipe/Qwen3-14B-Instruct": { - id: "OpenPipe/Qwen3-14B-Instruct", - name: "OpenPipe Qwen3 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 32768, output: 32768 }, - }, - "microsoft/Phi-4-mini-instruct": { - id: "microsoft/Phi-4-mini-instruct", - name: "Phi-4-mini-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.35 }, - limit: { context: 128000, output: 128000 }, - }, - "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8": { - id: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8", - name: "NVIDIA Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.65 }, - limit: { context: 161000, output: 161000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.85 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 196608 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 4.4, cache_read: 0.26, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - }, - }, - chutes: { - id: "chutes", - env: ["CHUTES_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://llm.chutes.ai/v1", - name: "Chutes", - doc: "https://llm.chutes.ai/v1/models", - models: { - "miromind-ai/MiroThinker-v1.5-235B": { - id: "miromind-ai/MiroThinker-v1.5-235B", - name: "MiroThinker V1.5 235B", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.15 }, - limit: { context: 262144, output: 8192 }, - }, - "OpenGVLab/InternVL3-78B-TEE": { - id: "OpenGVLab/InternVL3-78B-TEE", - name: "InternVL3 78B TEE", - family: "opengvlab", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-01-06", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.39 }, - limit: { context: 32768, output: 32768 }, - }, - "NousResearch/DeepHermes-3-Mistral-24B-Preview": { - id: "NousResearch/DeepHermes-3-Mistral-24B-Preview", - name: "DeepHermes 3 Mistral 24B Preview", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.1 }, - limit: { context: 32768, output: 32768 }, - }, - "NousResearch/Hermes-4-405B-FP8-TEE": { - id: "NousResearch/Hermes-4-405B-FP8-TEE", - name: "Hermes 4 405B FP8 TEE", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 131072, output: 65536 }, - }, - "NousResearch/Hermes-4.3-36B": { - id: "NousResearch/Hermes-4.3-36B", - name: "Hermes 4.3 36B", - family: "nousresearch", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.39 }, - limit: { context: 32768, output: 8192 }, - }, - "NousResearch/Hermes-4-14B": { - id: "NousResearch/Hermes-4-14B", - name: "Hermes 4 14B", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.05 }, - limit: { context: 40960, output: 40960 }, - }, - "NousResearch/Hermes-4-70B": { - id: "NousResearch/Hermes-4-70B", - name: "Hermes 4 70B", - family: "nousresearch", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.38 }, - limit: { context: 131072, output: 131072 }, - }, - "Qwen/Qwen3-30B-A3B": { - id: "Qwen/Qwen3-30B-A3B", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.22 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-Coder-Next": { - id: "Qwen/Qwen3-Coder-Next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3-235B-A22B": { - id: "Qwen/Qwen3-235B-A22B", - name: "Qwen3 235B A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct-TEE": { - id: "Qwen/Qwen2.5-VL-72B-Instruct-TEE", - name: "Qwen2.5 VL 72B Instruct TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3Guard-Gen-0.6B": { - id: "Qwen/Qwen3Guard-Gen-0.6B", - name: "Qwen3Guard Gen 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 32768, output: 8192 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.33 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.24, cache_read: 0.04 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen3 14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-TEE", - name: "Qwen3 235B A22B Instruct 2507 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.55, cache_read: 0.04 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 16384, output: 16384 }, - }, - "Qwen/Qwen3.5-397B-A17B-TEE": { - id: "Qwen/Qwen3.5-397B-A17B-TEE", - name: "Qwen3.5 397B A17B TEE", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 2.34, cache_read: 0.195 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE", - name: "Qwen3 Coder 480B A35B Instruct FP8 TEE", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.95, cache_read: 0.11 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 32768, output: 32768 }, - }, - "zai-org/GLM-5.1-TEE": { - id: "zai-org/GLM-5.1-TEE", - name: "GLM 5.1 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15, cache_read: 0.475 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM 4.7 Flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.35 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-TEE": { - id: "zai-org/GLM-4.5-TEE", - name: "GLM 4.5 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.55 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-4.6-FP8": { - id: "zai-org/GLM-4.6-FP8", - name: "GLM 4.6 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "GLM 4.5 Air", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 131072, output: 131072 }, - }, - "zai-org/GLM-4.7-FP8": { - id: "zai-org/GLM-4.7-FP8", - name: "GLM 4.7 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-5-TEE": { - id: "zai-org/GLM-5-TEE", - name: "GLM 5 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15, cache_read: 0.475 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.5-FP8": { - id: "zai-org/GLM-4.5-FP8", - name: "GLM 4.5 FP8", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-4.7-TEE": { - id: "zai-org/GLM-4.7-TEE", - name: "GLM 4.7 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.5 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "GLM 4.6V", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9, cache_read: 0.15 }, - limit: { context: 131072, output: 65536 }, - }, - "zai-org/GLM-5-Turbo": { - id: "zai-org/GLM-5-Turbo", - name: "GLM 5 Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 1.96, cache_read: 0.245 }, - limit: { context: 202752, output: 65535 }, - }, - "zai-org/GLM-4.6-TEE": { - id: "zai-org/GLM-4.6-TEE", - name: "GLM 4.6 TEE", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.7, cache_read: 0.2 }, - limit: { context: 202752, output: 65536 }, - }, - "mistralai/Devstral-2-123B-Instruct-2512-TEE": { - id: "mistralai/Devstral-2-123B-Instruct-2512-TEE", - name: "Devstral 2 123B Instruct 2512 TEE", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.22 }, - limit: { context: 262144, output: 65536 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29 }, - limit: { context: 262144, output: 32000 }, - }, - "chutesai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "chutesai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct 2506", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.18 }, - limit: { context: 131072, output: 131072 }, - }, - "chutesai/Mistral-Small-3.1-24B-Instruct-2503": { - id: "chutesai/Mistral-Small-3.1-24B-Instruct-2503", - name: "Mistral Small 3.1 24B Instruct 2503", - family: "chutesai", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11, cache_read: 0.015 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16": { - id: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", - name: "NVIDIA Nemotron 3 Nano 30B A3B BF16", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/DeepSeek-R1-TEE": { - id: "deepseek-ai/DeepSeek-R1-TEE", - name: "DeepSeek R1 TEE", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Llama-70B", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3.1-TEE": { - id: "deepseek-ai/DeepSeek-V3.1-TEE", - name: "DeepSeek V3.1 TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3-0324-TEE": { - id: "deepseek-ai/DeepSeek-V3-0324-TEE", - name: "DeepSeek V3 0324 TEE", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.19, output: 0.87, cache_read: 0.095 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.2-Speciale-TEE": { - id: "deepseek-ai/DeepSeek-V3.2-Speciale-TEE", - name: "DeepSeek V3.2 Speciale TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus-TEE": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus-TEE", - name: "DeepSeek V3.1 Terminus TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.23, output: 0.9 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-R1-0528-TEE": { - id: "deepseek-ai/DeepSeek-R1-0528-TEE", - name: "DeepSeek R1 0528 TEE", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.75 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/DeepSeek-V3.2-TEE": { - id: "deepseek-ai/DeepSeek-V3.2-TEE", - name: "DeepSeek V3.2 TEE", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.14 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "gpt oss 20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-oss-120b-TEE": { - id: "openai/gpt-oss-120b-TEE", - name: "gpt oss 120b TEE", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.18 }, - limit: { context: 131072, output: 65536 }, - }, - "unsloth/gemma-3-12b-it": { - id: "unsloth/gemma-3-12b-it", - name: "gemma 3 12b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "unsloth/Llama-3.2-3B-Instruct": { - id: "unsloth/Llama-3.2-3B-Instruct", - name: "Llama 3.2 3B Instruct", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-02-12", - last_updated: "2025-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 16384, output: 16384 }, - }, - "unsloth/gemma-3-4b-it": { - id: "unsloth/gemma-3-4b-it", - name: "gemma 3 4b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.03 }, - limit: { context: 96000, output: 96000 }, - }, - "unsloth/Llama-3.2-1B-Instruct": { - id: "unsloth/Llama-3.2-1B-Instruct", - name: "Llama 3.2 1B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 32768, output: 8192 }, - }, - "unsloth/Mistral-Nemo-Instruct-2407": { - id: "unsloth/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04, cache_read: 0.01 }, - limit: { context: 131072, output: 131072 }, - }, - "unsloth/Mistral-Small-24B-Instruct-2501": { - id: "unsloth/Mistral-Small-24B-Instruct-2501", - name: "Mistral Small 24B Instruct 2501", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11 }, - limit: { context: 32768, output: 32768 }, - }, - "unsloth/gemma-3-27b-it": { - id: "unsloth/gemma-3-27b-it", - name: "gemma 3 27b it", - family: "unsloth", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.15, cache_read: 0.02 }, - limit: { context: 128000, output: 65536 }, - }, - "moonshotai/Kimi-K2-Thinking-TEE": { - id: "moonshotai/Kimi-K2-Thinking-TEE", - name: "Kimi K2 Thinking TEE", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.75 }, - limit: { context: 262144, output: 65535 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.9, cache_read: 0.195 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.6-TEE": { - id: "moonshotai/Kimi-K2.6-TEE", - name: "Kimi K2.6 TEE", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-04-20", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.44, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5-TEE": { - id: "moonshotai/Kimi-K2.5-TEE", - name: "Kimi K2.5 TEE", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 65535 }, - }, - "MiniMaxAI/MiniMax-M2.1-TEE": { - id: "MiniMaxAI/MiniMax-M2.1-TEE", - name: "MiniMax M2.1 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.12 }, - limit: { context: 196608, output: 65536 }, - }, - "MiniMaxAI/MiniMax-M2.5-TEE": { - id: "MiniMaxAI/MiniMax-M2.5-TEE", - name: "MiniMax M2.5 TEE", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.1, cache_read: 0.15 }, - limit: { context: 196608, output: 65536 }, - }, - "rednote-hilab/dots.ocr": { - id: "rednote-hilab/dots.ocr", - name: "dots.ocr", - family: "rednote", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01, cache_read: 0.005 }, - limit: { context: 131072, output: 131072 }, - }, - "tngtech/TNG-R1T-Chimera-Turbo": { - id: "tngtech/TNG-R1T-Chimera-Turbo", - name: "TNG R1T Chimera Turbo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.6 }, - limit: { context: 163840, output: 65536 }, - }, - "tngtech/DeepSeek-TNG-R1T2-Chimera": { - id: "tngtech/DeepSeek-TNG-R1T2-Chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85 }, - limit: { context: 163840, output: 163840 }, - }, - "tngtech/DeepSeek-R1T-Chimera": { - id: "tngtech/DeepSeek-R1T-Chimera", - name: "DeepSeek R1T Chimera", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 163840, output: 163840 }, - }, - "tngtech/TNG-R1T-Chimera-TEE": { - id: "tngtech/TNG-R1T-Chimera-TEE", - name: "TNG R1T Chimera TEE", - family: "tngtech", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85 }, - limit: { context: 163840, output: 65536 }, - }, - }, - }, - dinference: { - id: "dinference", - env: ["DINFERENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.dinference.com/v1", - name: "DInference", - doc: "https://dinference.com", - models: { - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08", - last_updated: "2025-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0675, output: 0.27 }, - limit: { context: 131072, output: 32768 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.65 }, - limit: { context: 200000, output: 128000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 2.4 }, - limit: { context: 200000, output: 128000 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 3.89 }, - limit: { context: 200000, output: 128000 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 200000, output: 32000 }, - }, - }, - }, - vivgrid: { - id: "vivgrid", - env: ["VIVGRID_API_KEY"], - npm: "@ai-sdk/openai", - api: "https://api.vivgrid.com/v1", - name: "Vivgrid", - doc: "https://docs.vivgrid.com/models", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42 }, - limit: { context: 128000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - provider: { npm: "@ai-sdk/openai-compatible" }, - }, - }, - }, - deepinfra: { - id: "deepinfra", - env: ["DEEPINFRA_API_KEY"], - npm: "@ai-sdk/deepinfra", - name: "Deep Infra", - doc: "https://deepinfra.com/models", - models: { - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen 3.5 397B A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-01", - last_updated: "2026-04-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.54, output: 3.4 }, - limit: { context: 262144, output: 81920 }, - }, - "Qwen/Qwen3.5-35B-A3B": { - id: "Qwen/Qwen3.5-35B-A3B", - name: "Qwen 3.5 35B A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-01", - last_updated: "2026-04-20", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.95 }, - limit: { context: 262144, output: 81920 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", - name: "Qwen3 Coder 480B A35B Instruct Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.6 }, - limit: { context: 262144, output: 66536 }, - }, - "Qwen/Qwen3.6-35B-A3B": { - id: "Qwen/Qwen3.6-35B-A3B", - name: "Qwen3.6 35B A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1 }, - limit: { context: 262144, output: 81920 }, - }, - "zai-org/GLM-4.7-Flash": { - id: "zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-4.5": { - id: "zai-org/GLM-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 98304 }, - status: "deprecated", - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 1.75, cache_read: 0.08 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.56, cache_read: 0.16 }, - limit: { context: 202752, output: 16384 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.43, output: 1.74, cache_read: 0.08 }, - limit: { context: 204800, output: 131072 }, - }, - "meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama 4 Scout 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 10000000, output: 16384 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.1-70B-Instruct": { - id: "meta-llama/Llama-3.1-70B-Instruct", - name: "Llama 3.1 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.1-8B-Instruct-Turbo": { - id: "meta-llama/Llama-3.1-8B-Instruct-Turbo", - name: "Llama 3.1 8B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.32 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama 4 Maverick 17B FP8", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1000000, output: 16384 }, - }, - "meta-llama/Llama-3.1-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.1-70B-Instruct-Turbo", - name: "Llama 3.1 70B Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek-R1-0528", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.15, cache_read: 0.35 }, - limit: { context: 163840, output: 64000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.26, output: 0.38, cache_read: 0.13 }, - limit: { context: 163840, output: 64000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 16384 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.24 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-06", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.47, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 3.5, cache_read: 0.15 }, - limit: { context: 262144, output: 16384 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.8 }, - limit: { context: 262144, output: 32768 }, - }, - "MiniMaxAI/MiniMax-M2": { - id: "MiniMaxAI/MiniMax-M2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.254, output: 1.02 }, - limit: { context: 262144, output: 32768 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-06", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.95, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.2 }, - limit: { context: 196608, output: 196608 }, - }, - "anthropic/claude-3-7-sonnet-latest": { - id: "anthropic/claude-3-7-sonnet-latest", - name: "Claude Sonnet 3.7 (Latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.3, output: 16.5, cache_read: 0.33 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-4-opus": { - id: "anthropic/claude-4-opus", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-06-12", - last_updated: "2025-06-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 82.5 }, - limit: { context: 200000, output: 32000 }, - }, - "deepseek-ai/DeepSeek-V4-Flash": { - id: "deepseek-ai/DeepSeek-V4-Flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 65536, output: 65536 }, - }, - "google/gemma-4-26B-A4B-it": { - id: "google/gemma-4-26B-A4B-it", - name: "Gemma 4 26B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.34 }, - limit: { context: 256000, output: 8192 }, - }, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.38 }, - limit: { context: 256000, output: 8192 }, - }, - }, - }, - "qiniu-ai": { - id: "qiniu-ai", - env: ["QINIU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.qnaigc.com/v1", - name: "Qiniu", - doc: "https://developer.qiniu.com/aitokenapi", - models: { - "qwen3-235b-a22b": { - id: "qwen3-235b-a22b", - name: "Qwen 3 235B A22B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "doubao-seed-1.6-flash": { - id: "doubao-seed-1.6-flash", - name: "Doubao-Seed 1.6 Flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235b A22B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 64000 }, - }, - "doubao-seed-2.0-code": { - id: "doubao-seed-2.0-code", - name: "Doubao Seed 2.0 Code", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 128000 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "doubao-1.5-thinking-pro": { - id: "doubao-1.5-thinking-pro", - name: "Doubao 1.5 Thinking Pro", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "claude-3.7-sonnet": { - id: "claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-22", - last_updated: "2026-02-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 64000 }, - }, - "qwen-vl-max-2025-01-25": { - id: "qwen-vl-max-2025-01-25", - name: "Qwen VL-MAX-2025-01-25", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 40000, output: 4096 }, - }, - "doubao-1.5-pro-32k": { - id: "doubao-1.5-pro-32k", - name: "Doubao 1.5 Pro 32k", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 12000 }, - }, - "qwen2.5-vl-72b-instruct": { - id: "qwen2.5-vl-72b-instruct", - name: "Qwen 2.5 VL 72B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 8192 }, - }, - "qwen3-vl-30b-a3b-thinking": { - id: "qwen3-vl-30b-a3b-thinking", - name: "Qwen3-Vl 30b A3b Thinking", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-09", - last_updated: "2026-02-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "gemini-3.0-pro-image-preview": { - id: "gemini-3.0-pro-image-preview", - name: "Gemini 3.0 Pro Image Preview", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 65536 }, - }, - "claude-4.5-opus": { - id: "claude-4.5-opus", - name: "Claude 4.5 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "claude-4.0-opus": { - id: "claude-4.0-opus", - name: "Claude 4.0 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 32000 }, - }, - "claude-4.5-haiku": { - id: "claude-4.5-haiku", - name: "Claude 4.5 Haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-16", - last_updated: "2025-10-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 65536 }, - }, - "gemini-3.0-flash-preview": { - id: "gemini-3.0-flash-preview", - name: "Gemini 3.0 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 64000 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "Gemini 2.5 Flash Image", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 32768, output: 8192 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 98304 }, - }, - "claude-3.5-sonnet": { - id: "claude-3.5-sonnet", - name: "Claude 3.5 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-09", - last_updated: "2025-09-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 8200 }, - }, - "claude-4.0-sonnet": { - id: "claude-4.0-sonnet", - name: "Claude 4.0 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30b A3b Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "doubao-seed-1.6-thinking": { - id: "doubao-seed-1.6-thinking", - name: "Doubao-Seed 1.6 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 64000 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262144, output: 4096 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 32768 }, - }, - "qwen3-30b-a3b-thinking-2507": { - id: "qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30b A3b Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 126000, output: 32000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131000, output: 4096 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 40000, output: 4096 }, - }, - "claude-4.1-opus": { - id: "claude-4.1-opus", - name: "Claude 4.1 Opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 32000 }, - }, - "doubao-seed-2.0-mini": { - id: "doubao-seed-2.0-mini", - name: "Doubao Seed 2.0 Mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 32768 }, - }, - "doubao-seed-1.6": { - id: "doubao-seed-1.6", - name: "Doubao-Seed 1.6", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-15", - last_updated: "2025-08-15", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen2.5-vl-7b-instruct": { - id: "qwen2.5-vl-7b-instruct", - name: "Qwen 2.5 VL 7B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "kling-v2-6": { - id: "kling-v2-6", - name: "Kling-V2 6", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-01-13", - last_updated: "2026-01-13", - modalities: { input: ["text", "image", "video"], output: ["video"] }, - open_weights: false, - limit: { context: 99999999, output: 99999999 }, - }, - "MiniMax-M1": { - id: "MiniMax-M1", - name: "MiniMax M1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 80000 }, - }, - "gemini-3.0-pro-preview": { - id: "gemini-3.0-pro-preview", - name: "Gemini 3.0 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "video", "pdf", "audio"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 64000 }, - }, - "doubao-seed-2.0-lite": { - id: "doubao-seed-2.0-lite", - name: "Doubao Seed 2.0 Lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-14", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 262000, output: 4096 }, - }, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 8192 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen-Turbo", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 4096 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 128000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 64000 }, - }, - "qwen3-max-preview": { - id: "qwen3-max-preview", - name: "Qwen3 Max Preview", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-06", - last_updated: "2025-09-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 64000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "doubao-1.5-vision-pro": { - id: "doubao-1.5-vision-pro", - name: "Doubao 1.5 Vision Pro", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "claude-4.5-sonnet": { - id: "claude-4.5-sonnet", - name: "Claude 4.5 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 64000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek-V3", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 1048576, output: 8192 }, - }, - "qwen-max-2025-01-25": { - id: "qwen-max-2025-01-25", - name: "Qwen2.5-Max-2025-01-25", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 4096 }, - }, - "doubao-seed-2.0-pro": { - id: "doubao-seed-2.0-pro", - name: "Doubao Seed 2.0 Pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 128000 }, - }, - "deepseek/deepseek-v3.2-exp-thinking": { - id: "deepseek/deepseek-v3.2-exp-thinking", - name: "DeepSeek/DeepSeek-V3.2-Exp-Thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.1-terminus-thinking": { - id: "deepseek/deepseek-v3.1-terminus-thinking", - name: "DeepSeek/DeepSeek-V3.1-Terminus-Thinking", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek/DeepSeek-V3.2-Exp", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-v3.2-251201": { - id: "deepseek/deepseek-v3.2-251201", - name: "Deepseek/DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "deepseek/deepseek-math-v2": { - id: "deepseek/deepseek-math-v2", - name: "Deepseek/Deepseek-Math-V2", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 160000, output: 160000 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek/DeepSeek-V3.1-Terminus", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 32000 }, - }, - "stepfun-ai/gelab-zero-4b-preview": { - id: "stepfun-ai/gelab-zero-4b-preview", - name: "Stepfun-Ai/Gelab Zero 4b Preview", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 8192, output: 4096 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "Stepfun/Step-3.5 Flash", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 64000, output: 4096 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "x-AI/Grok-4-Fast", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-20", - last_updated: "2025-09-20", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "x-AI/Grok-Code-Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-02", - last_updated: "2025-09-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-4-fast-reasoning": { - id: "x-ai/grok-4-fast-reasoning", - name: "X-Ai/Grok-4-Fast-Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast-non-reasoning": { - id: "x-ai/grok-4.1-fast-non-reasoning", - name: "X-Ai/Grok 4.1 Fast Non Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "x-AI/Grok-4.1-Fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2025-11-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4-fast-non-reasoning": { - id: "x-ai/grok-4-fast-non-reasoning", - name: "X-Ai/Grok-4-Fast-Non-Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-4.1-fast-reasoning": { - id: "x-ai/grok-4.1-fast-reasoning", - name: "X-Ai/Grok 4.1 Fast Reasoning", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 20000000, output: 2000000 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI/GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI/GPT-5", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 400000, output: 128000 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z-Ai/GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z-Ai/GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "z-ai/autoglm-phone-9b": { - id: "z-ai/autoglm-phone-9b", - name: "Z-Ai/Autoglm Phone 9b", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 12800, output: 4096 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z-AI/GLM 4.6", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-10-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 200000 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "Minimax/Minimax-M2", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 128000 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "Minimax/Minimax-M2.1", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "Minimax/Minimax-M2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "Minimax/Minimax-M2.5 Highspeed", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-14", - last_updated: "2026-02-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 204800, output: 128000 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Moonshotai/Kimi-K2.5", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-09-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 100000 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 100000 }, - }, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "Meituan/Longcat-Flash-Chat", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-11-05", - last_updated: "2025-11-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 131072, output: 131072 }, - }, - "meituan/longcat-flash-lite": { - id: "meituan/longcat-flash-lite", - name: "Meituan/Longcat-Flash-Lite", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 320000 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "Mimo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 256000, output: 256000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi/Mimo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 256000, output: 256000 }, - }, - }, - }, - kilo: { - id: "kilo", - env: ["KILO_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.kilo.ai/api/gateway", - name: "Kilo Gateway", - doc: "https://kilo.ai", - models: { - "rekaai/reka-edge": { - id: "rekaai/reka-edge", - name: "Reka Edge", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-20", - last_updated: "2026-04-11", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 16384, output: 16384 }, - }, - "rekaai/reka-flash-3": { - id: "rekaai/reka-flash-3", - name: "Reka Flash 3", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-03-12", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 65536, output: 65536 }, - }, - "ai21/jamba-large-1.7": { - id: "ai21/jamba-large-1.7", - name: "AI21: Jamba Large 1.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 256000, output: 4096 }, - }, - "alibaba/tongyi-deepresearch-30b-a3b": { - id: "alibaba/tongyi-deepresearch-30b-a3b", - name: "Tongyi DeepResearch 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.45 }, - limit: { context: 131072, output: 131072 }, - }, - "inflection/inflection-3-pi": { - id: "inflection/inflection-3-pi", - name: "Inflection: Inflection 3 Pi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 1024 }, - }, - "inflection/inflection-3-productivity": { - id: "inflection/inflection-3-productivity", - name: "Inflection: Inflection 3 Productivity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 1024 }, - }, - "liquid/lfm-2-24b-a2b": { - id: "liquid/lfm-2-24b-a2b", - name: "LiquidAI: LFM2-24B-A2B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.12 }, - limit: { context: 32768, output: 32768 }, - }, - "writer/palmyra-x5": { - id: "writer/palmyra-x5", - name: "Writer: Palmyra X5", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 6 }, - limit: { context: 1040000, output: 8192 }, - }, - "ibm-granite/granite-4.1-8b": { - id: "ibm-granite/granite-4.1-8b", - name: "IBM: Granite 4.1 8B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-30", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.1, cache_read: 0.05 }, - limit: { context: 131072, output: 131072 }, - }, - "ibm-granite/granite-4.0-h-micro": { - id: "ibm-granite/granite-4.0-h-micro", - name: "IBM: Granite 4.0 Micro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.017, output: 0.11 }, - limit: { context: 131000, output: 32768 }, - }, - "essentialai/rnj-1-instruct": { - id: "essentialai/rnj-1-instruct", - name: "EssentialAI: Rnj 1 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 6554 }, - }, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Perplexity: Sonar Pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar-deep-research": { - id: "perplexity/sonar-deep-research", - name: "Perplexity: Sonar Deep Research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 25600 }, - }, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Perplexity: Sonar", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127072, output: 25415 }, - }, - "perplexity/sonar-pro-search": { - id: "perplexity/sonar-pro-search", - name: "Perplexity: Sonar Pro Search", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-10-31", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar-reasoning-pro": { - id: "perplexity/sonar-reasoning-pro", - name: "Perplexity: Sonar Reasoning Pro", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 25600 }, - }, - "deepseek/deepseek-chat-v3.1": { - id: "deepseek/deepseek-chat-v3.1", - name: "DeepSeek: DeepSeek V3.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 32768, output: 7168 }, - }, - "deepseek/deepseek-chat": { - id: "deepseek/deepseek-chat", - name: "DeepSeek: DeepSeek V3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.32, output: 0.89, cache_read: 0.15 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-llama-70b": { - id: "deepseek/deepseek-r1-distill-llama-70b", - name: "DeepSeek: R1 Distill Llama 70B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 0.8, cache_read: 0.015 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek: R1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.5 }, - limit: { context: 64000, output: 16000 }, - }, - "deepseek/deepseek-v3.2-speciale": { - id: "deepseek/deepseek-v3.2-speciale", - name: "DeepSeek: DeepSeek V3.2 Speciale", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.2, cache_read: 0.135 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-r1-distill-qwen-32b": { - id: "deepseek/deepseek-r1-distill-qwen-32b", - name: "DeepSeek: R1 Distill Qwen 32B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.29 }, - limit: { context: 32768, output: 32768 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek: DeepSeek V3.2 Exp", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek: DeepSeek V4 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.28, cache_read: 0.0028 }, - limit: { context: 1048576, output: 384000 }, - }, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek: DeepSeek V4 Pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.435, output: 0.87, cache_read: 0.003625 }, - limit: { context: 1048576, output: 384000 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek: DeepSeek V3.2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.38, cache_read: 0.125 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-chat-v3-0324": { - id: "deepseek/deepseek-chat-v3-0324", - name: "DeepSeek: DeepSeek V3 0324", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.77, cache_read: 0.095 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek: R1 0528", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.15, cache_read: 0.2 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek: DeepSeek V3.1 Terminus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.21, output: 0.79, cache_read: 0.13 }, - limit: { context: 163840, output: 32768 }, - }, - "openrouter/auto": { - id: "openrouter/auto", - name: "Auto Router", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 2000000, output: 32768 }, - }, - "openrouter/bodybuilder": { - id: "openrouter/bodybuilder", - name: "Body Builder (beta)", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - status: "beta", - }, - "openrouter/owl-alpha": { - id: "openrouter/owl-alpha", - name: "Owl Alpha", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1048756, output: 262144 }, - status: "alpha", - }, - "openrouter/pareto-code": { - id: "openrouter/pareto-code", - name: "Pareto Code Router", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2026-04-21", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 65536 }, - }, - "openrouter/free": { - id: "openrouter/free", - name: "Free Models Router", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 32768 }, - }, - "inclusionai/ling-2.6-1t:free": { - id: "inclusionai/ling-2.6-1t:free", - name: "inclusionAI: Ling-2.6-1T (free)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-23", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "inclusionai/ling-2.6-flash": { - id: "inclusionai/ling-2.6-flash", - name: "inclusionAI: Ling-2.6 Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.24, cache_read: 0.016 }, - limit: { context: 262144, output: 32768 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Arcee AI: Trinity Mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "arcee-ai/virtuoso-large": { - id: "arcee-ai/virtuoso-large", - name: "Arcee AI: Virtuoso Large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 1.2 }, - limit: { context: 131072, output: 64000 }, - }, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Arcee AI: Trinity Large Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.85 }, - limit: { context: 262144, output: 262144 }, - }, - "arcee-ai/spotlight": { - id: "arcee-ai/spotlight", - name: "Arcee AI: Spotlight", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131072, output: 65537 }, - }, - "arcee-ai/maestro-reasoning": { - id: "arcee-ai/maestro-reasoning", - name: "Arcee AI: Maestro Reasoning", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 3.3 }, - limit: { context: 131072, output: 32000 }, - }, - "arcee-ai/coder-large": { - id: "arcee-ai/coder-large", - name: "Arcee AI: Coder Large", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Arcee AI: Trinity Large Preview", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.45 }, - limit: { context: 131000, output: 32768 }, - }, - "deepcogito/cogito-v2.1-671b": { - id: "deepcogito/cogito-v2.1-671b", - name: "Deep Cogito: Cogito v2.1 671B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, output: 32768 }, - }, - "upstage/solar-pro-3": { - id: "upstage/solar-pro-3", - name: "Upstage: Solar Pro 3", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 32768 }, - }, - "nex-agi/deepseek-v3.1-nex-n1": { - id: "nex-agi/deepseek-v3.1-nex-n1", - name: "Nex AGI: DeepSeek V3.1 Nex N1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 163840 }, - }, - "bytedance-seed/seed-1.6": { - id: "bytedance-seed/seed-1.6", - name: "ByteDance Seed: Seed 1.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 32768 }, - }, - "bytedance-seed/seed-2.0-lite": { - id: "bytedance-seed/seed-2.0-lite", - name: "ByteDance Seed: Seed-2.0-Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2 }, - limit: { context: 262144, output: 131072 }, - }, - "bytedance-seed/seed-1.6-flash": { - id: "bytedance-seed/seed-1.6-flash", - name: "ByteDance Seed: Seed 1.6 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 262144, output: 32768 }, - }, - "bytedance-seed/seed-2.0-mini": { - id: "bytedance-seed/seed-2.0-mini", - name: "ByteDance Seed: Seed-2.0-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "mancer/weaver": { - id: "mancer/weaver", - name: "Mancer: Weaver (alpha)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-08-02", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 1 }, - limit: { context: 8000, output: 2000 }, - }, - "anthracite-org/magnum-v4-72b": { - id: "anthracite-org/magnum-v4-72b", - name: "Magnum v4 72B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 5 }, - limit: { context: 16384, output: 2048 }, - }, - "~google/gemini-pro-latest": { - id: "~google/gemini-pro-latest", - name: "Google: Gemini Pro Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "~google/gemini-flash-latest": { - id: "~google/gemini-flash-latest", - name: "Google: Gemini Flash Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 0.08333333333333334 }, - limit: { context: 1048576, output: 65536 }, - }, - "kilo-auto/balanced": { - id: "kilo-auto/balanced", - name: "Kilo Auto Balanced", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3 }, - limit: { context: 204800, output: 131072 }, - }, - "kilo-auto/frontier": { - id: "kilo-auto/frontier", - name: "Kilo Auto Frontier", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25 }, - limit: { context: 1000000, output: 128000 }, - }, - "kilo-auto/small": { - id: "kilo-auto/small", - name: "Kilo Auto Small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 400000, output: 128000 }, - }, - "kilo-auto/free": { - id: "kilo-auto/free", - name: "Kilo Auto Free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "undi95/remm-slerp-l2-13b": { - id: "undi95/remm-slerp-l2-13b", - name: "ReMM SLERP 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-07-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 0.65 }, - limit: { context: 6144, output: 4096 }, - }, - "allenai/olmo-3-32b-think": { - id: "allenai/olmo-3-32b-think", - name: "AllenAI: Olmo 3 32B Think", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.5 }, - limit: { context: 65536, output: 65536 }, - }, - "nousresearch/hermes-2-pro-llama-3-8b": { - id: "nousresearch/hermes-2-pro-llama-3-8b", - name: "NousResearch: Hermes 2 Pro - Llama-3 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-05-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - }, - "nousresearch/hermes-4-405b": { - id: "nousresearch/hermes-4-405b", - name: "Nous: Hermes 4 405B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 26215 }, - }, - "nousresearch/hermes-3-llama-3.1-70b": { - id: "nousresearch/hermes-3-llama-3.1-70b", - name: "Nous: Hermes 3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 131072, output: 32768 }, - }, - "nousresearch/hermes-4-70b": { - id: "nousresearch/hermes-4-70b", - name: "Nous: Hermes 4 70B", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-08-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, cache_read: 0.055 }, - limit: { context: 131072, output: 131072 }, - }, - "nousresearch/hermes-3-llama-3.1-405b": { - id: "nousresearch/hermes-3-llama-3.1-405b", - name: "Nous: Hermes 3 405B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 1 }, - limit: { context: 131072, output: 16384 }, - }, - "morph/morph-v3-fast": { - id: "morph/morph-v3-fast", - name: "Morph: Morph V3 Fast", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 81920, output: 38000 }, - }, - "morph/morph-v3-large": { - id: "morph/morph-v3-large", - name: "Morph: Morph V3 Large", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 262144, output: 131072 }, - }, - "stepfun/step-3.5-flash:free": { - id: "stepfun/step-3.5-flash:free", - name: "StepFun: Step 3.5 Flash (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-26", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "stepfun/step-3.5-flash": { - id: "stepfun/step-3.5-flash", - name: "StepFun: Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, output: 256000 }, - }, - "alpindale/goliath-120b": { - id: "alpindale/goliath-120b", - name: "Goliath 120B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-11-10", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3.75, output: 7.5 }, - limit: { context: 6144, output: 1024 }, - }, - "mistralai/mistral-nemo": { - id: "mistralai/mistral-nemo", - name: "Mistral: Mistral Nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-01", - last_updated: "2024-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 131072, output: 16384 }, - }, - "mistralai/mistral-saba": { - id: "mistralai/mistral-saba", - name: "Mistral: Saba", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 32768, output: 32768 }, - }, - "mistralai/mistral-large-2512": { - id: "mistralai/mistral-large-2512", - name: "Mistral: Mistral Large 3 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-01", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 52429 }, - }, - "mistralai/devstral-medium": { - id: "mistralai/devstral-medium", - name: "Mistral: Devstral Medium", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-small-3.1-24b-instruct": { - id: "mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral: Mistral Small 3.1 24B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.56, cache_read: 0.015 }, - limit: { context: 128000, output: 131072 }, - }, - "mistralai/mistral-medium-3-5": { - id: "mistralai/mistral-medium-3-5", - name: "Mistral: Mistral Medium 3.5", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-30", - last_updated: "2026-05-07", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/pixtral-large-2411": { - id: "mistralai/pixtral-large-2411", - name: "Mistral: Pixtral Large 2411", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/devstral-2512": { - id: "mistralai/devstral-2512", - name: "Mistral: Devstral 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.025 }, - limit: { context: 262144, output: 65536 }, - }, - "mistralai/codestral-2508": { - id: "mistralai/codestral-2508", - name: "Mistral: Codestral 2508", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 51200 }, - }, - "mistralai/mistral-small-24b-instruct-2501": { - id: "mistralai/mistral-small-24b-instruct-2501", - name: "Mistral: Mistral Small 3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-29", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 32768, output: 16384 }, - }, - "mistralai/mistral-large-2411": { - id: "mistralai/mistral-large-2411", - name: "Mistral Large 2411", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-11-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mixtral-8x22b-instruct": { - id: "mistralai/mixtral-8x22b-instruct", - name: "Mistral: Mixtral 8x22B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 65536, output: 13108 }, - }, - "mistralai/mistral-large-2407": { - id: "mistralai/mistral-large-2407", - name: "Mistral Large 2407", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-19", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/ministral-8b-2512": { - id: "mistralai/ministral-8b-2512", - name: "Mistral: Ministral 3 8B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, output: 32768 }, - }, - "mistralai/mistral-medium-3.1": { - id: "mistralai/mistral-medium-3.1", - name: "Mistral: Mistral Medium 3.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-small-2603": { - id: "mistralai/mistral-small-2603", - name: "Mistral: Mistral Small 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-04-11", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6, cache_read: 0.015 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/ministral-3b-2512": { - id: "mistralai/ministral-3b-2512", - name: "Mistral: Ministral 3 3B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 32768 }, - }, - "mistralai/voxtral-small-24b-2507": { - id: "mistralai/voxtral-small-24b-2507", - name: "Mistral: Voxtral Small 24B 2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 32000, output: 6400 }, - }, - "mistralai/mixtral-8x7b-instruct": { - id: "mistralai/mixtral-8x7b-instruct", - name: "Mistral: Mixtral 8x7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-12-10", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.54, output: 0.54 }, - limit: { context: 32768, output: 16384 }, - }, - "mistralai/mistral-medium-3": { - id: "mistralai/mistral-medium-3", - name: "Mistral: Mistral Medium 3", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-small-3.2-24b-instruct": { - id: "mistralai/mistral-small-3.2-24b-instruct", - name: "Mistral: Mistral Small 3.2 24B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.18, cache_read: 0.03 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/devstral-small": { - id: "mistralai/devstral-small", - name: "Mistral: Devstral Small 1.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-05-07", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 26215 }, - }, - "mistralai/mistral-large": { - id: "mistralai/mistral-large", - name: "Mistral Large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 25600 }, - }, - "mistralai/mistral-7b-instruct-v0.1": { - id: "mistralai/mistral-7b-instruct-v0.1", - name: "Mistral: Mistral 7B Instruct v0.1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.19 }, - limit: { context: 2824, output: 565 }, - }, - "mistralai/ministral-14b-2512": { - id: "mistralai/ministral-14b-2512", - name: "Mistral: Ministral 3 14B 2512", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, output: 52429 }, - }, - "~anthropic/claude-haiku-latest": { - id: "~anthropic/claude-haiku-latest", - name: "Anthropic: Claude Haiku Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "~anthropic/claude-sonnet-latest": { - id: "~anthropic/claude-sonnet-latest", - name: "Anthropic: Claude Sonnet Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 128000 }, - }, - "~anthropic/claude-opus-latest": { - id: "~anthropic/claude-opus-latest", - name: "Anthropic: Claude Opus Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-16", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "meta-llama/llama-3.3-70b-instruct": { - id: "meta-llama/llama-3.3-70b-instruct", - name: "Meta: Llama 3.3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.32 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/llama-4-scout": { - id: "meta-llama/llama-4-scout", - name: "Meta: Llama 4 Scout", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 327680, output: 16384 }, - }, - "meta-llama/llama-guard-3-8b": { - id: "meta-llama/llama-guard-3-8b", - name: "Llama Guard 3 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-18", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06 }, - limit: { context: 131072, output: 26215 }, - }, - "meta-llama/llama-4-maverick": { - id: "meta-llama/llama-4-maverick", - name: "Meta: Llama 4 Maverick", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-12-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 1048576, output: 16384 }, - }, - "meta-llama/llama-3.2-11b-vision-instruct": { - id: "meta-llama/llama-3.2-11b-vision-instruct", - name: "Meta: Llama 3.2 11B Vision Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.049, output: 0.049 }, - limit: { context: 131072, output: 16384 }, - }, - "meta-llama/llama-guard-4-12b": { - id: "meta-llama/llama-guard-4-12b", - name: "Meta: Llama Guard 4 12B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 163840, output: 32768 }, - }, - "meta-llama/llama-3.1-70b-instruct": { - id: "meta-llama/llama-3.1-70b-instruct", - name: "Meta: Llama 3.1 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 26215 }, - }, - "meta-llama/llama-3.2-1b-instruct": { - id: "meta-llama/llama-3.2-1b-instruct", - name: "Meta: Llama 3.2 1B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-18", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.027, output: 0.2 }, - limit: { context: 60000, output: 12000 }, - }, - "meta-llama/llama-3.2-3b-instruct": { - id: "meta-llama/llama-3.2-3b-instruct", - name: "Meta: Llama 3.2 3B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-09-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 80000, output: 16384 }, - }, - "meta-llama/llama-3-8b-instruct": { - id: "meta-llama/llama-3-8b-instruct", - name: "Meta: Llama 3 8B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-25", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.04 }, - limit: { context: 8192, output: 16384 }, - }, - "meta-llama/llama-3.1-8b-instruct": { - id: "meta-llama/llama-3.1-8b-instruct", - name: "Meta: Llama 3.1 8B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.05 }, - limit: { context: 16384, output: 16384 }, - }, - "meta-llama/llama-3-70b-instruct": { - id: "meta-llama/llama-3-70b-instruct", - name: "Meta: Llama 3 70B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "x-ai/grok-4.20": { - id: "x-ai/grok-4.20", - name: "xAI: Grok 4.20", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-31", - last_updated: "2026-04-11", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-code-fast-1:optimized:free": { - id: "x-ai/grok-code-fast-1:optimized:free", - name: "xAI: Grok Code Fast 1 Optimized (experimental, free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-4.3": { - id: "x-ai/grok-4.3", - name: "xAI: Grok 4.3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-05-01", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 2.5, cache_read: 0.2 }, - limit: { context: 1000000, output: 4096 }, - }, - "x-ai/grok-4-fast": { - id: "x-ai/grok-4-fast", - name: "xAI: Grok 4 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-code-fast-1": { - id: "x-ai/grok-code-fast-1", - name: "xAI: Grok Code Fast 1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "x-ai/grok-3-beta": { - id: "x-ai/grok-3-beta", - name: "xAI: Grok 3 Beta", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "xAI: Grok 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 51200 }, - }, - "x-ai/grok-3-mini": { - id: "x-ai/grok-3-mini", - name: "xAI: Grok 3 Mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-4.1-fast": { - id: "x-ai/grok-4.1-fast", - name: "xAI: Grok 4.1 Fast", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "x-ai/grok-3-mini-beta": { - id: "x-ai/grok-3-mini-beta", - name: "xAI: Grok 3 Mini Beta", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 26215 }, - }, - "x-ai/grok-4.20-multi-agent": { - id: "x-ai/grok-4.20-multi-agent", - name: "xAI: Grok 4.20 Multi-Agent", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-31", - last_updated: "2026-04-11", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 2000000, output: 2000000 }, - }, - "x-ai/grok-3": { - id: "x-ai/grok-3", - name: "xAI: Grok 3", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 26215 }, - }, - "tencent/hy3-preview:free": { - id: "tencent/hy3-preview:free", - name: "Tencent: Hy3 Preview (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "tencent/hunyuan-a13b-instruct": { - id: "tencent/hunyuan-a13b-instruct", - name: "Tencent: Hunyuan A13B Instruct", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131072, output: 131072 }, - }, - "gryphe/mythomax-l2-13b": { - id: "gryphe/mythomax-l2-13b", - name: "MythoMax 13B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-25", - last_updated: "2024-04-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 4096, output: 4096 }, - }, - "sao10k/l3-euryale-70b": { - id: "sao10k/l3-euryale-70b", - name: "Sao10k: Llama 3 Euryale 70B v2.1", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-06-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.48, output: 1.48 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3-lunaris-8b": { - id: "sao10k/l3-lunaris-8b", - name: "Sao10K: Llama 3 8B Lunaris", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-08-13", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.05 }, - limit: { context: 8192, output: 8192 }, - }, - "sao10k/l3.3-euryale-70b": { - id: "sao10k/l3.3-euryale-70b", - name: "Sao10K: Llama 3.3 Euryale 70B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-12-18", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 0.75 }, - limit: { context: 131072, output: 16384 }, - }, - "sao10k/l3.1-70b-hanami-x1": { - id: "sao10k/l3.1-70b-hanami-x1", - name: "Sao10K: Llama 3.1 70B Hanami x1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-01-08", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 16000, output: 16000 }, - }, - "sao10k/l3.1-euryale-70b": { - id: "sao10k/l3.1-euryale-70b", - name: "Sao10K: Llama 3.1 Euryale 70B v2.2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 0.85 }, - limit: { context: 131072, output: 16384 }, - }, - "microsoft/wizardlm-2-8x22b": { - id: "microsoft/wizardlm-2-8x22b", - name: "WizardLM-2 8x22B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-04-24", - last_updated: "2024-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 0.62 }, - limit: { context: 65535, output: 8000 }, - }, - "microsoft/phi-4-mini-instruct": { - id: "microsoft/phi-4-mini-instruct", - name: "Microsoft: Phi 4 Mini Instruct", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-10-17", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.35, cache_read: 0.08 }, - limit: { context: 128000, output: 128000 }, - }, - "microsoft/phi-4": { - id: "microsoft/phi-4", - name: "Microsoft: Phi 4", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.14 }, - limit: { context: 16384, output: 16384 }, - }, - "poolside/laguna-m.1:free": { - id: "poolside/laguna-m.1:free", - name: "Poolside: Laguna M.1 (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "poolside/laguna-xs.2:free": { - id: "poolside/laguna-xs.2:free", - name: "Poolside: Laguna XS.2 (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "cohere/command-r7b-12-2024": { - id: "cohere/command-r7b-12-2024", - name: "Cohere: Command R7B (12-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-02-27", - last_updated: "2024-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "cohere/command-a": { - id: "cohere/command-a", - name: "Cohere: Command A", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8192 }, - }, - "cohere/command-r-plus-08-2024": { - id: "cohere/command-r-plus-08-2024", - name: "Cohere: Command R+ (08-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "cohere/command-r-08-2024": { - id: "cohere/command-r-08-2024", - name: "Cohere: Command R (08-2024)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "Prime Intellect: INTELLECT-3", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-26", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "NVIDIA: Llama 3.3 Nemotron Super 49B V1.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 131072, output: 26215 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA: Nemotron 3 Super", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", - name: "NVIDIA: Nemotron 3 Nano Omni (free)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-05-01", - modalities: { input: ["text", "audio", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 65536 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "NVIDIA: Nemotron 3 Nano 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 262144, output: 52429 }, - }, - "nvidia/nemotron-3-super-120b-a12b:free": { - id: "nvidia/nemotron-3-super-120b-a12b:free", - name: "NVIDIA: Nemotron 3 Super (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 26215 }, - }, - "nvidia/llama-3.1-nemotron-70b-instruct": { - id: "nvidia/llama-3.1-nemotron-70b-instruct", - name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-10-12", - last_updated: "2024-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 131072, output: 16384 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Inception: Mercury 2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "OpenAI: GPT-5.1-Codex-Max", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "OpenAI: GPT-5.2 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "OpenAI: GPT-4o-mini Search Preview", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "OpenAI: GPT-5 Chat", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-2024-05-13": { - id: "openai/gpt-4o-2024-05-13", - name: "OpenAI: GPT-4o (2024-05-13)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "OpenAI: GPT-5.3 Chat", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2026-03-04", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "OpenAI: GPT-5.2 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4-1106-preview": { - id: "openai/gpt-4-1106-preview", - name: "OpenAI: GPT-4 Turbo (older v1106)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-11-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-chat-latest": { - id: "openai/gpt-chat-latest", - name: "OpenAI: GPT Chat Latest", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - release_date: "2026-05-05", - last_updated: "2026-05-07", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-audio-preview": { - id: "openai/gpt-4o-audio-preview", - name: "OpenAI: GPT-4o Audio", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-15", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "OpenAI: GPT-5.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-24", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "OpenAI: GPT-5 Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "OpenAI: GPT-5 Nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "OpenAI: GPT-5.3-Codex", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-02-25", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-3.5-turbo-16k": { - id: "openai/gpt-3.5-turbo-16k", - name: "OpenAI: GPT-3.5 Turbo 16k", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-08-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "OpenAI: GPT-4 Turbo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-09-13", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "OpenAI: GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "OpenAI: o3 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "OpenAI: o3 Mini High", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2026-03-15", - modalities: { input: ["pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "OpenAI: GPT-4o-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.075 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "OpenAI: o4 Mini Deep Research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-06-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "OpenAI: GPT-5.4 Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-17", - last_updated: "2026-04-11", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "OpenAI: GPT-5.1 Chat", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI: o4 Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.275 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "OpenAI: GPT-5.4 Nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-17", - last_updated: "2026-04-11", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "OpenAI: GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-mini-2024-07-18": { - id: "openai/gpt-4o-mini-2024-07-18", - name: "OpenAI: GPT-4o-mini (2024-07-18)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1-Codex-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-4o-2024-08-06": { - id: "openai/gpt-4o-2024-08-06", - name: "OpenAI: GPT-4o (2024-08-06)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-08-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "OpenAI: GPT-5 Image", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 10, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "OpenAI: GPT-5.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI: o1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-05", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "OpenAI: GPT-5.4 Pro", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-03-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "OpenAI: GPT-3.5 Turbo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "OpenAI: o3 Deep Research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-06-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI: o3 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-20", - last_updated: "2026-03-15", - modalities: { input: ["pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4-turbo-preview": { - id: "openai/gpt-4-turbo-preview", - name: "OpenAI: GPT-4 Turbo Preview", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "OpenAI: o1-pro", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - release_date: "2025-03-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-image-2": { - id: "openai/gpt-5.4-image-2", - name: "OpenAI: GPT-5.4 Image 2", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - release_date: "2026-04-21", - last_updated: "2026-05-01", - modalities: { input: ["image", "text", "pdf"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 8, output: 15, cache_read: 2 }, - limit: { context: 272000, output: 128000 }, - }, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "OpenAI: GPT-4", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-03-14", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8191, output: 4096 }, - }, - "openai/gpt-4-0314": { - id: "openai/gpt-4-0314", - name: "OpenAI: GPT-4 (older v0314)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-05-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8191, output: 4096 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "OpenAI: GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-03-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-audio": { - id: "openai/gpt-audio", - name: "OpenAI: GPT Audio", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-search-preview": { - id: "openai/gpt-4o-search-preview", - name: "OpenAI: GPT-4o Search Preview", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "OpenAI: GPT-4.1 Nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/o4-mini-high": { - id: "openai/o4-mini-high", - name: "OpenAI: o4 Mini High", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2025-04-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI: o3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "OpenAI: gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 26215 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-audio-mini": { - id: "openai/gpt-audio-mini", - name: "OpenAI: GPT Audio Mini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "OpenAI: GPT-4o", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-3.5-turbo-0613": { - id: "openai/gpt-3.5-turbo-0613", - name: "OpenAI: GPT-3.5 Turbo (older v0613)", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 4095, output: 4096 }, - }, - "openai/gpt-5-image-mini": { - id: "openai/gpt-5-image-mini", - name: "OpenAI: GPT-5 Image Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-16", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 2.5, output: 2 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "OpenAI: GPT-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "OpenAI: gpt-oss-safeguard-20b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-29", - last_updated: "2025-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3, cache_read: 0.037 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI: gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.039, output: 0.19 }, - limit: { context: 131072, output: 26215 }, - }, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "OpenAI: GPT-5.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-24", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, output: 128000 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "OpenAI: GPT-3.5 Turbo Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2023-03-01", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4095, output: 4096 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "OpenAI: GPT-4.1", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "OpenAI: GPT-4.1 Mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "OpenAI: GPT-5.1-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-2024-11-20": { - id: "openai/gpt-4o-2024-11-20", - name: "OpenAI: GPT-4o (2024-11-20)", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-20", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "amazon/nova-lite-v1": { - id: "amazon/nova-lite-v1", - name: "Amazon: Nova Lite 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 300000, output: 5120 }, - }, - "amazon/nova-pro-v1": { - id: "amazon/nova-pro-v1", - name: "Amazon: Nova Pro 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 300000, output: 5120 }, - }, - "amazon/nova-premier-v1": { - id: "amazon/nova-premier-v1", - name: "Amazon: Nova Premier 1.0", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-11-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 12.5 }, - limit: { context: 1000000, output: 32000 }, - }, - "amazon/nova-2-lite-v1": { - id: "amazon/nova-2-lite-v1", - name: "Amazon: Nova 2 Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 65535 }, - }, - "amazon/nova-micro-v1": { - id: "amazon/nova-micro-v1", - name: "Amazon: Nova Micro 1.0", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14 }, - limit: { context: 128000, output: 5120 }, - }, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "Z.ai: GLM 5V Turbo", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-11", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.7": { - id: "z-ai/glm-4.7", - name: "Z.ai: GLM 4.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.38, output: 1.98, cache_read: 0.2 }, - limit: { context: 202752, output: 65535 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "Z.ai: GLM 5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 2.3 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 32768 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "Z.ai: GLM 5.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.26, output: 3.96 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5": { - id: "z-ai/glm-4.5", - name: "Z.ai: GLM 4.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.175 }, - limit: { context: 131072, output: 98304 }, - }, - "z-ai/glm-4.5-air": { - id: "z-ai/glm-4.5-air", - name: "Z.ai: GLM 4.5 Air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.85, cache_read: 0.025 }, - limit: { context: 131072, output: 98304 }, - }, - "z-ai/glm-5-turbo": { - id: "z-ai/glm-5-turbo", - name: "Z.ai: GLM 5 Turbo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 202752, output: 131072 }, - }, - "z-ai/glm-4.5v": { - id: "z-ai/glm-4.5v", - name: "Z.ai: GLM 4.5V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.11 }, - limit: { context: 65536, output: 16384 }, - }, - "z-ai/glm-4.6": { - id: "z-ai/glm-4.6", - name: "Z.ai: GLM 4.6", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.9, cache_read: 0.175 }, - limit: { context: 204800, output: 204800 }, - }, - "z-ai/glm-4.6v": { - id: "z-ai/glm-4.6v", - name: "Z.ai: GLM 4.6V", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-30", - last_updated: "2026-01-10", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131072, output: 131072 }, - }, - "z-ai/glm-4.7-flash": { - id: "z-ai/glm-4.7-flash", - name: "Z.ai: GLM 4.7 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.01 }, - limit: { context: 202752, output: 40551 }, - }, - "baidu/ernie-4.5-vl-424b-a47b": { - id: "baidu/ernie-4.5-vl-424b-a47b", - name: "Baidu: ERNIE 4.5 VL 424B A47B ", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2026-01", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.42, output: 1.25 }, - limit: { context: 123000, output: 16000 }, - }, - "baidu/qianfan-ocr-fast:free": { - id: "baidu/qianfan-ocr-fast:free", - name: "Baidu: Qianfan-OCR-Fast (free)", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-05-01", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 28672 }, - }, - "baidu/cobuddy:free": { - id: "baidu/cobuddy:free", - name: "Baidu: CoBuddy (free)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-05-06", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 65536 }, - }, - "baidu/ernie-4.5-vl-28b-a3b": { - id: "baidu/ernie-4.5-vl-28b-a3b", - name: "Baidu: ERNIE 4.5 VL 28B A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.56 }, - limit: { context: 30000, output: 8000 }, - }, - "baidu/ernie-4.5-21b-a3b": { - id: "baidu/ernie-4.5-21b-a3b", - name: "Baidu: ERNIE 4.5 21B A3B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-06-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 120000, output: 8000 }, - }, - "baidu/ernie-4.5-300b-a47b": { - id: "baidu/ernie-4.5-300b-a47b", - name: "Baidu: ERNIE 4.5 300B A47B ", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-06-30", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 123000, output: 12000 }, - }, - "baidu/ernie-4.5-21b-a3b-thinking": { - id: "baidu/ernie-4.5-21b-a3b-thinking", - name: "Baidu: ERNIE 4.5 21B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131072, output: 65536 }, - }, - "relace/relace-apply-3": { - id: "relace/relace-apply-3", - name: "Relace: Relace Apply 3", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-09-26", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 1.25 }, - limit: { context: 256000, output: 128000 }, - }, - "relace/relace-search": { - id: "relace/relace-search", - name: "Relace: Relace Search", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3 }, - limit: { context: 256000, output: 128000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "MiniMax: MiniMax M2.7", - family: "minimax-m2.7", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax: MiniMax M2", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.255, output: 1, cache_read: 0.03 }, - limit: { context: 196608, output: 196608 }, - }, - "minimax/minimax-01": { - id: "minimax/minimax-01", - name: "MiniMax: MiniMax-01", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000192, output: 1000192 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax: MiniMax M2.1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.95, cache_read: 0.03 }, - limit: { context: 196608, output: 39322 }, - }, - "minimax/minimax-m1": { - id: "minimax/minimax-m1", - name: "MiniMax: MiniMax M1", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.2 }, - limit: { context: 1000000, output: 40000 }, - }, - "minimax/minimax-m2-her": { - id: "minimax/minimax-m2-her", - name: "MiniMax: MiniMax M2-her", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 65536, output: 2048 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax: MiniMax M2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.2, cache_read: 0.029 }, - limit: { context: 196608, output: 196608 }, - }, - "~openai/gpt-latest": { - id: "~openai/gpt-latest", - name: "OpenAI: GPT Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 1050000, output: 128000 }, - }, - "~openai/gpt-mini-latest": { - id: "~openai/gpt-mini-latest", - name: "OpenAI: GPT Mini Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen: Qwen3 235B A22B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.455, output: 1.82, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen: Qwen3.5-122B-A10B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.08 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-coder-plus": { - id: "qwen/qwen3-coder-plus", - name: "Qwen: Qwen3 Coder Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 3.25, cache_read: 0.2 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.6-27b": { - id: "qwen/qwen3.6-27b", - name: "Qwen: Qwen3.6 27B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.325, output: 3.25 }, - limit: { context: 256000, output: 65536 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen: Qwen3.5-27B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.195, output: 1.56 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-235b-a22b-2507": { - id: "qwen/qwen3-235b-a22b-2507", - name: "Qwen: Qwen3 235B A22B Instruct 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.071, output: 0.1 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen3-8b": { - id: "qwen/qwen3-8b", - name: "Qwen: Qwen3 8B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.4, cache_read: 0.05 }, - limit: { context: 40960, output: 8192 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen: Qwen3.5 397B A17B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39, output: 2.34 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen-vl-plus": { - id: "qwen/qwen-vl-plus", - name: "Qwen: Qwen VL Plus", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-01-25", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1365, output: 0.4095, cache_read: 0.042 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen: Qwen3 32B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.24, cache_read: 0.04 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen2.5-vl-72b-instruct": { - id: "qwen/qwen2.5-vl-72b-instruct", - name: "Qwen: Qwen2.5 VL 72B Instruct", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-02-01", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen/qwen-max": { - id: "qwen/qwen-max", - name: "Qwen: Qwen-Max ", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-03", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.04, output: 4.16, cache_read: 0.32 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen-plus": { - id: "qwen/qwen-plus", - name: "Qwen: Qwen-Plus", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.2, cache_read: 0.08 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen3.6-35b-a3b": { - id: "qwen/qwen3.6-35b-a3b", - name: "Qwen: Qwen3.6 35B A3B", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1612, output: 0.96525, cache_read: 0.1612 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-vl-235b-a22b-thinking": { - id: "qwen/qwen3-vl-235b-a22b-thinking", - name: "Qwen: Qwen3 VL 235B A22B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-24", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 2.6 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-vl-30b-a3b-thinking": { - id: "qwen/qwen3-vl-30b-a3b-thinking", - name: "Qwen: Qwen3 VL 30B A3B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 1.56 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-vl-8b-instruct": { - id: "qwen/qwen3-vl-8b-instruct", - name: "Qwen: Qwen3 VL 8B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.5 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3.5-flash-02-23": { - id: "qwen/qwen3.5-flash-02-23", - name: "Qwen: Qwen3.5-Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.6-plus": { - id: "qwen/qwen3.6-plus", - name: "Qwen: Qwen3.6 Plus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-26", - last_updated: "2026-04-11", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.325, output: 1.95, cache_read: 0.0325, cache_write: 0.40625 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-max": { - id: "qwen/qwen3-max", - name: "Qwen: Qwen3 Max", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen-plus-2025-07-28": { - id: "qwen/qwen-plus-2025-07-28", - name: "Qwen: Qwen Plus 0728", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.78 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-instruct-2507": { - id: "qwen/qwen3-30b-a3b-instruct-2507", - name: "Qwen: Qwen3 30B A3B Instruct 2507", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.3, cache_read: 0.04 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-vl-32b-instruct": { - id: "qwen/qwen3-vl-32b-instruct", - name: "Qwen: Qwen3 VL 32B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.104, output: 0.416 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-235b-a22b-thinking-2507": { - id: "qwen/qwen3-235b-a22b-thinking-2507", - name: "Qwen: Qwen3 235B A22B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-25", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen: Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0975, output: 0.78 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-30b-a3b-thinking-2507": { - id: "qwen/qwen3-30b-a3b-thinking-2507", - name: "Qwen: Qwen3 30B A3B Thinking 2507", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen-2.5-7b-instruct": { - id: "qwen/qwen-2.5-7b-instruct", - name: "Qwen: Qwen2.5 7B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.1 }, - limit: { context: 32768, output: 6554 }, - }, - "qwen/qwen-vl-max": { - id: "qwen/qwen-vl-max", - name: "Qwen: Qwen VL Max", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-flash": { - id: "qwen/qwen3-coder-flash", - name: "Qwen: Qwen3 Coder Flash", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.195, output: 0.975, cache_read: 0.06 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-30b-a3b": { - id: "qwen/qwen3-30b-a3b", - name: "Qwen: Qwen3 30B A3B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.28, cache_read: 0.03 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen: Qwen3 Next 80B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 1.1 }, - limit: { context: 131072, output: 52429 }, - }, - "qwen/qwen3.5-plus-20260420": { - id: "qwen/qwen3.5-plus-20260420", - name: "Qwen: Qwen3.5 Plus 2026-04-20", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3-coder-next": { - id: "qwen/qwen3-coder-next", - name: "Qwen: Qwen3 Coder Next", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.75, cache_read: 0.035 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen-2.5-coder-32b-instruct": { - id: "qwen/qwen-2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32B Instruct", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-11-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2, cache_read: 0.015 }, - limit: { context: 32768, output: 8192 }, - }, - "qwen/qwen3-vl-30b-a3b-instruct": { - id: "qwen/qwen3-vl-30b-a3b-instruct", - name: "Qwen: Qwen3 VL 30B A3B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b-a3b-instruct": { - id: "qwen/qwen3-coder-30b-a3b-instruct", - name: "Qwen: Qwen3 Coder 30B A3B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "qwen/qwen3-max-thinking": { - id: "qwen/qwen3-max-thinking", - name: "Qwen: Qwen3 Max Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.78, output: 3.9 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen/qwen-turbo": { - id: "qwen/qwen-turbo", - name: "Qwen: Qwen-Turbo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-01", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0325, output: 0.13, cache_read: 0.01 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen3-vl-235b-a22b-instruct": { - id: "qwen/qwen3-vl-235b-a22b-instruct", - name: "Qwen: Qwen3 VL 235B A22B Instruct", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-23", - last_updated: "2026-01-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.88, cache_read: 0.11 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen: Qwen3 Coder 480B A35B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1, cache_read: 0.022 }, - limit: { context: 262144, output: 52429 }, - }, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen: Qwen3.5-9B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.15 }, - limit: { context: 256000, output: 32768 }, - }, - "qwen/qwen3-vl-8b-thinking": { - id: "qwen/qwen3-vl-8b-thinking", - name: "Qwen: Qwen3 VL 8B Thinking", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.117, output: 1.365 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3.6-max-preview": { - id: "qwen/qwen3.6-max-preview", - name: "Qwen: Qwen3.6 Max Preview", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.04, output: 6.24, cache_write: 1.3 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen-plus-2025-07-28:thinking": { - id: "qwen/qwen-plus-2025-07-28:thinking", - name: "Qwen: Qwen Plus 0728 (thinking)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.78 }, - limit: { context: 1000000, output: 32768 }, - }, - "qwen/qwen-2.5-72b-instruct": { - id: "qwen/qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.39 }, - limit: { context: 32768, output: 16384 }, - }, - "qwen/qwen3-14b": { - id: "qwen/qwen3-14b", - name: "Qwen: Qwen3 14B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24, cache_read: 0.025 }, - limit: { context: 40960, output: 40960 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen: Qwen3.5-35B-A3B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1625, output: 1.3 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3.5-plus-02-15": { - id: "qwen/qwen3.5-plus-02-15", - name: "Qwen: Qwen3.5 Plus 2026-02-15", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-03-15", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.26, output: 1.56 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen/qwen3.6-flash": { - id: "qwen/qwen3.6-flash", - name: "Qwen: Qwen3.6 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_write: 0.3125 }, - limit: { context: 1000000, output: 65536 }, - }, - "alfredpros/codellama-7b-instruct-solidity": { - id: "alfredpros/codellama-7b-instruct-solidity", - name: "AlfredPros: CodeLLaMa 7B Instruct Solidity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-14", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 4096, output: 4096 }, - }, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kwaipilot: KAT-Coder-Pro V2", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 80000 }, - }, - "google/gemini-2.5-pro-preview-05-06": { - id: "google/gemini-2.5-pro-preview-05-06", - name: "Google: Gemini 2.5 Pro Preview 05-06", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-06", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/lyria-3-clip-preview": { - id: "google/lyria-3-clip-preview", - name: "Google: Lyria 3 Clip Preview", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-04-11", - modalities: { input: ["image", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-pro-preview-customtools": { - id: "google/gemini-3.1-pro-preview-customtools", - name: "Google: Gemini 3.1 Pro Preview Custom Tools", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Google: Gemini 2.5 Flash Lite Preview 09-2025", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, reasoning: 0.4, cache_read: 0.01, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash-001": { - id: "google/gemini-2.0-flash-001", - name: "Google: Gemini 2.0 Flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-11", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025, cache_write: 0.083333 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/lyria-3-pro-preview": { - id: "google/lyria-3-pro-preview", - name: "Google: Lyria 3 Pro Preview", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-04-11", - modalities: { input: ["image", "text"], output: ["audio", "text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Google: Gemma 3n 4B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 32768, output: 6554 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Google: Gemini 3.1 Flash Lite Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, reasoning: 1.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Google: Gemini 3.1 Pro Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Google: Gemini 3 Flash Preview", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, reasoning: 3, cache_read: 0.05, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Google: Gemini 2.5 Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-20", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-image-preview": { - id: "google/gemini-3-pro-image-preview", - name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-11-20", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 2, output: 12, reasoning: 12 }, - limit: { context: 65536, output: 32768 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Google: Gemma 4 31B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-11", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.4 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemini-2.5-flash-image": { - id: "google/gemini-2.5-flash-image", - name: "Google: Nano Banana (Gemini 2.5 Flash Image)", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-08", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Google: Gemma 3 12B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.13, cache_read: 0.015 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Google: Gemini 2.5 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, reasoning: 2.5, cache_read: 0.03, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/gemini-3.1-flash-image-preview": { - id: "google/gemini-3.1-flash-image-preview", - name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["image", "text"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 65536, output: 65536 }, - }, - "google/gemma-3-4b-it": { - id: "google/gemma-3-4b-it", - name: "Google: Gemma 3 4B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-13", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.08 }, - limit: { context: 131072, output: 19200 }, - }, - "google/gemini-2.5-pro-preview": { - id: "google/gemini-2.5-pro-preview", - name: "Google: Gemini 2.5 Pro Preview 06-05", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-05", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, reasoning: 10, cache_read: 0.125, cache_write: 0.375 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemma-2-27b-it": { - id: "google/gemma-2-27b-it", - name: "Google: Gemma 2 27B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-06-24", - last_updated: "2024-06-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 0.65 }, - limit: { context: 8192, output: 2048 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Google: Gemma 3 27B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-12", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.11, cache_read: 0.02 }, - limit: { context: 128000, output: 65536 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Google: Gemma 4 26B A4B", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-11", - modalities: { input: ["image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Google: Gemini 2.5 Flash Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-17", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, reasoning: 0.4, cache_read: 0.01, cache_write: 0.083333 }, - limit: { context: 1048576, output: 65535 }, - }, - "google/gemini-2.0-flash-lite-001": { - id: "google/gemini-2.0-flash-lite-001", - name: "Google: Gemini 2.0 Flash Lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-11", - last_updated: "2026-03-15", - modalities: { input: ["audio", "image", "pdf", "text", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "MoonshotAI: Kimi K2.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.2 }, - limit: { context: 262144, output: 65535 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "MoonshotAI: Kimi K2 0905", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.15 }, - limit: { context: 131072, output: 26215 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "MoonshotAI: Kimi K2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - structured_output: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "MoonshotAI: Kimi K2 0711", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131000, output: 26215 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "MoonshotAI: Kimi K2 Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-06", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.47, output: 2, cache_read: 0.2 }, - limit: { context: 131072, output: 65535 }, - }, - "aion-labs/aion-1.0": { - id: "aion-labs/aion-1.0", - name: "AionLabs: Aion-1.0", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 4, output: 8 }, - limit: { context: 131072, output: 32768 }, - }, - "aion-labs/aion-rp-llama-3.1-8b": { - id: "aion-labs/aion-rp-llama-3.1-8b", - name: "AionLabs: Aion-RP 1.0 (8B)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.6 }, - limit: { context: 32768, output: 32768 }, - }, - "aion-labs/aion-2.0": { - id: "aion-labs/aion-2.0", - name: "AionLabs: Aion-2.0", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.6 }, - limit: { context: 131072, output: 32768 }, - }, - "aion-labs/aion-1.0-mini": { - id: "aion-labs/aion-1.0-mini", - name: "AionLabs: Aion-1.0-Mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-02-05", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 1.4 }, - limit: { context: 131072, output: 32768 }, - }, - "~moonshotai/kimi-latest": { - id: "~moonshotai/kimi-latest", - name: "MoonshotAI: Kimi Latest", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-27", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.74, output: 3.49, cache_read: 0.14 }, - limit: { context: 262142, output: 262142 }, - }, - "thedrummer/unslopnemo-12b": { - id: "thedrummer/unslopnemo-12b", - name: "TheDrummer: UnslopNemo 12B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-11-09", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 32768, output: 32768 }, - }, - "thedrummer/cydonia-24b-v4.1": { - id: "thedrummer/cydonia-24b-v4.1", - name: "TheDrummer: Cydonia 24B V4.1", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-27", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "thedrummer/skyfall-36b-v2": { - id: "thedrummer/skyfall-36b-v2", - name: "TheDrummer: Skyfall 36B V2", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-11", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "thedrummer/rocinante-12b": { - id: "thedrummer/rocinante-12b", - name: "TheDrummer: Rocinante 12B", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-09-30", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.43 }, - limit: { context: 32768, output: 32768 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Anthropic: Claude Opus 4.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.7-sonnet:thinking": { - id: "anthropic/claude-3.7-sonnet:thinking", - name: "Anthropic: Claude 3.7 Sonnet (thinking)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4.6-fast": { - id: "anthropic/claude-opus-4.6-fast", - name: "Anthropic: Claude Opus 4.6 (Fast)", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-04-07", - last_updated: "2026-04-11", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 150, cache_read: 3, cache_write: 37.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-02-19", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Anthropic: Claude Opus 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Anthropic: Claude Opus 4.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-16", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Anthropic: Claude Sonnet 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Anthropic: Claude Opus 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-11-24", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Anthropic: Claude 3 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-03-07", - last_updated: "2024-03-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Anthropic: Claude Opus 4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2026-03-15", - modalities: { input: ["image", "pdf", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Anthropic: Claude Haiku 4.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Anthropic: Claude Sonnet 4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 1000000, output: 128000 }, - }, - "switchpoint/router": { - id: "switchpoint/router", - name: "Switchpoint Router", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-07-12", - last_updated: "2026-03-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 3.4 }, - limit: { context: 131072, output: 32768 }, - }, - "bytedance/ui-tars-1.5-7b": { - id: "bytedance/ui-tars-1.5-7b", - name: "ByteDance: UI-TARS 7B ", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-07-23", - last_updated: "2026-03-15", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.2 }, - limit: { context: 128000, output: 2048 }, - }, - "tngtech/deepseek-r1t2-chimera": { - id: "tngtech/deepseek-r1t2-chimera", - name: "TNG: DeepSeek R1T2 Chimera", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.85, cache_read: 0.125 }, - limit: { context: 163840, output: 163840 }, - }, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "Xiaomi: MiMo V2.5 Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-omni": { - id: "xiaomi/mimo-v2-omni", - name: "Xiaomi: MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 65536 }, - }, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "Xiaomi: MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "Xiaomi: MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.29, cache_read: 0.045 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - "sap-ai-core": { - id: "sap-ai-core", - env: ["AICORE_SERVICE_KEY"], - npm: "@jerome-benoit/sap-ai-provider-v2", - name: "SAP AI Core", - doc: "https://help.sap.com/docs/sap-ai-core", - models: { - "anthropic--claude-4.6-opus": { - id: "anthropic--claude-4.6-opus", - name: "anthropic--claude-4.6-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic--claude-3-haiku": { - id: "anthropic--claude-3-haiku", - name: "anthropic--claude-3-haiku", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic--claude-3-opus": { - id: "anthropic--claude-3-opus", - name: "anthropic--claude-3-opus", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "gpt-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "gpt-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "gemini-2.5-pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-3.7-sonnet": { - id: "anthropic--claude-3.7-sonnet", - name: "anthropic--claude-3.7-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "sonar-pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic--claude-4.5-sonnet": { - id: "anthropic--claude-4.5-sonnet", - name: "anthropic--claude-4.5-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic--claude-4.6-sonnet": { - id: "anthropic--claude-4.6-sonnet", - name: "anthropic--claude-4.6-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "sonar-deep-research", - family: "sonar-deep-research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-02-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, reasoning: 3 }, - limit: { context: 128000, output: 32768 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "gemini-2.5-flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-25", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-4.5-opus": { - id: "anthropic--claude-4.5-opus", - name: "anthropic--claude-4.5-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - sonar: { - id: "sonar", - name: "sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic--claude-4-opus": { - id: "anthropic--claude-4-opus", - name: "anthropic--claude-4-opus", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic--claude-3-sonnet": { - id: "anthropic--claude-3-sonnet", - name: "anthropic--claude-3-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic--claude-4-sonnet": { - id: "anthropic--claude-4-sonnet", - name: "anthropic--claude-4-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "gemini-2.5-flash-lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic--claude-4.5-haiku": { - id: "anthropic--claude-4.5-haiku", - name: "anthropic--claude-4.5-haiku", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5": { - id: "gpt-5", - name: "gpt-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "gpt-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "gpt-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "anthropic--claude-3.5-sonnet": { - id: "anthropic--claude-3.5-sonnet", - name: "anthropic--claude-3.5-sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - }, - }, - morph: { - id: "morph", - env: ["MORPH_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.morphllm.com/v1", - name: "Morph", - doc: "https://docs.morphllm.com/api-reference/introduction", - models: { - auto: { - id: "auto", - name: "Auto", - family: "auto", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 1.55 }, - limit: { context: 32000, output: 32000 }, - }, - "morph-v3-fast": { - id: "morph-v3-fast", - name: "Morph v3 Fast", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 16000, output: 16000 }, - }, - "morph-v3-large": { - id: "morph-v3-large", - name: "Morph v3 Large", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 32000, output: 32000 }, - }, - }, - }, - "cloudflare-ai-gateway": { - id: "cloudflare-ai-gateway", - env: ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_GATEWAY_ID"], - npm: "ai-gateway-provider", - name: "Cloudflare AI Gateway", - doc: "https://developers.cloudflare.com/ai-gateway/", - models: { - "workers-ai/@cf/myshell-ai/melotts": { - id: "workers-ai/@cf/myshell-ai/melotts", - name: "MyShell MeloTTS", - family: "melotts", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/ibm-granite/granite-4.0-h-micro": { - id: "workers-ai/@cf/ibm-granite/granite-4.0-h-micro", - name: "IBM Granite 4.0 H Micro", - family: "granite", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.017, output: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/huggingface/distilbert-sst-2-int8": { - id: "workers-ai/@cf/huggingface/distilbert-sst-2-int8", - name: "DistilBERT SST-2 INT8", - family: "distilbert", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.026, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/zai-org/glm-4.7-flash": { - id: "workers-ai/@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "workers-ai/@cf/pipecat-ai/smart-turn-v2": { - id: "workers-ai/@cf/pipecat-ai/smart-turn-v2", - name: "Pipecat Smart Turn v2", - family: "smart-turn", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct": { - id: "workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct", - name: "Mistral Small 3.1 24B Instruct", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/facebook/bart-large-cnn": { - id: "workers-ai/@cf/facebook/bart-large-cnn", - name: "BART Large CNN", - family: "bart", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-09", - last_updated: "2025-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it": { - id: "workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it", - name: "Gemma SEA-LION v4 27B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { - id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": { - id: "workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", - name: "DeepSeek R1 Distill Qwen 32B", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 4.88 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/openai/gpt-oss-20b": { - id: "workers-ai/@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/openai/gpt-oss-120b": { - id: "workers-ai/@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/mistral/mistral-7b-instruct-v0.1": { - id: "workers-ai/@cf/mistral/mistral-7b-instruct-v0.1", - name: "Mistral 7B Instruct v0.1", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.11, output: 0.19 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3-8b-instruct-awq": { - id: "workers-ai/@cf/meta/llama-3-8b-instruct-awq", - name: "Llama 3 8B Instruct AWQ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-guard-3-8b": { - id: "workers-ai/@cf/meta/llama-guard-3-8b", - name: "Llama Guard 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.48, output: 0.03 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/m2m100-1.2b": { - id: "workers-ai/@cf/meta/m2m100-1.2b", - name: "M2M100 1.2B", - family: "m2m", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-2-7b-chat-fp16": { - id: "workers-ai/@cf/meta/llama-2-7b-chat-fp16", - name: "Llama 2 7B Chat FP16", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 6.67 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-11b-vision-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049, output: 0.68 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast": { - id: "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast", - name: "Llama 3.3 70B Instruct FP8 Fast", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 2.25 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-1b-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.027, output: 0.2 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8", - name: "Llama 3.1 8B Instruct FP8", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.29 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.2-3b-instruct": { - id: "workers-ai/@cf/meta/llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct-awq": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct-awq", - name: "Llama 3.1 8B Instruct AWQ", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3-8b-instruct": { - id: "workers-ai/@cf/meta/llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.83 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/meta/llama-3.1-8b-instruct": { - id: "workers-ai/@cf/meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.8299999999999998 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct": { - id: "workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct", - name: "Qwen 2.5 Coder 32B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 1 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen3-embedding-0.6b": { - id: "workers-ai/@cf/qwen/qwen3-embedding-0.6b", - name: "Qwen3 Embedding 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.012, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwq-32b": { - id: "workers-ai/@cf/qwen/qwq-32b", - name: "QwQ 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.66, output: 1 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/qwen/qwen3-30b-a3b-fp8": { - id: "workers-ai/@cf/qwen/qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B FP8", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.051, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/google/gemma-3-12b-it": { - id: "workers-ai/@cf/google/gemma-3-12b-it", - name: "Gemma 3 12B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.56 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/moonshotai/kimi-k2.5": { - id: "workers-ai/@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - "workers-ai/@cf/moonshotai/kimi-k2.6": { - id: "workers-ai/@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 256000, output: 256000 }, - }, - "workers-ai/@cf/ai4bharat/indictrans2-en-indic-1B": { - id: "workers-ai/@cf/ai4bharat/indictrans2-en-indic-1B", - name: "IndicTrans2 EN-Indic 1B", - family: "indictrans", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 0.34 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/pfnet/plamo-embedding-1b": { - id: "workers-ai/@cf/pfnet/plamo-embedding-1b", - name: "PLaMo Embedding 1B", - family: "plamo", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.019, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-small-en-v1.5": { - id: "workers-ai/@cf/baai/bge-small-en-v1.5", - name: "BGE Small EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-large-en-v1.5": { - id: "workers-ai/@cf/baai/bge-large-en-v1.5", - name: "BGE Large EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-reranker-base": { - id: "workers-ai/@cf/baai/bge-reranker-base", - name: "BGE Reranker Base", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-09", - last_updated: "2025-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0031, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-base-en-v1.5": { - id: "workers-ai/@cf/baai/bge-base-en-v1.5", - name: "BGE Base EN v1.5", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.067, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/baai/bge-m3": { - id: "workers-ai/@cf/baai/bge-m3", - name: "BGE M3", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.012, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/aura-2-en": { - id: "workers-ai/@cf/deepgram/aura-2-en", - name: "Deepgram Aura 2 (EN)", - family: "aura", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/aura-2-es": { - id: "workers-ai/@cf/deepgram/aura-2-es", - name: "Deepgram Aura 2 (ES)", - family: "aura", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "workers-ai/@cf/deepgram/nova-3": { - id: "workers-ai/@cf/deepgram/nova-3", - name: "Deepgram Nova 3", - family: "nova", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5-turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2021-09-01", - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 1.25 }, - limit: { context: 16385, output: 4096 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4": { - id: "openai/gpt-4", - name: "GPT-4", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "ai-gateway-provider" }, - }, - "anthropic/claude-opus-4-7": { - id: "anthropic/claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "anthropic/claude-opus-4-1": { - id: "anthropic/claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3-5-haiku": { - id: "anthropic/claude-3-5-haiku", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3-sonnet": { - id: "anthropic/claude-3-sonnet", - name: "Claude Sonnet 3", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-3-opus": { - id: "anthropic/claude-3-opus", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - }, - }, - "github-copilot": { - id: "github-copilot", - env: ["GITHUB_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.githubcopilot.com", - name: "GitHub Copilot", - doc: "https://docs.github.com/en/copilot", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1-Codex-max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-12-04", - last_updated: "2025-12-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - status: "deprecated", - }, - "claude-opus-4.6": { - id: "claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 64000 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - status: "deprecated", - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4.7": { - id: "claude-opus-4.7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 64000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - status: "deprecated", - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 216000, input: 128000, output: 16000 }, - status: "deprecated", - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-27", - last_updated: "2025-08-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 128000, output: 64000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 264000, input: 128000, output: 64000 }, - status: "deprecated", - }, - "claude-sonnet-4.5": { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 32000 }, - }, - "claude-opus-41": { - id: "claude-opus-41", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 80000, output: 16000 }, - status: "deprecated", - }, - "claude-opus-4.5": { - id: "claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 160000, input: 128000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 64000, output: 4096 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - status: "deprecated", - }, - "claude-haiku-4.5": { - id: "claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 144000, input: 128000, output: 32000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, input: 64000, output: 16384 }, - }, - "claude-sonnet-4.6": { - id: "claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, input: 128000, output: 32000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 128000, output: 128000 }, - status: "deprecated", - }, - }, - }, - mixlayer: { - id: "mixlayer", - env: ["MIXLAYER_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.mixlayer.ai/v1", - name: "Mixlayer", - doc: "https://docs.mixlayer.com", - models: { - "qwen/qwen3.5-122b-a10b": { - id: "qwen/qwen3.5-122b-a10b", - name: "Qwen3.5 122B A10B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 3.2 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-27b": { - id: "qwen/qwen3.5-27b", - name: "Qwen3.5 27B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 2.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-9b": { - id: "qwen/qwen3.5-9b", - name: "Qwen3.5 9B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen/qwen3.5-35b-a3b": { - id: "qwen/qwen3.5-35b-a3b", - name: "Qwen3.5 35B A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.3 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "xiaomi-token-plan-sgp": { - id: "xiaomi-token-plan-sgp", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-sgp.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (Singapore)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 16384 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - }, - }, - zai: { - id: "zai", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.z.ai/api/paas/v4", - name: "Z.AI", - doc: "https://docs.z.ai/guides/overview/pricing", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 4.4, cache_read: 0.26, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.03, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - }, - }, - opencode: { - id: "opencode", - env: ["OPENCODE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://opencode.ai/zen/v1", - name: "OpenCode Zen", - doc: "https://opencode.ai/docs/zen", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.08 }, - limit: { context: 262144, output: 65536 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.7-free": { - id: "glm-4.7-free", - name: "GLM-4.7 Free", - family: "glm-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "gemini-3.1-pro": { - id: "gemini-3.1-pro", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/google" }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "kimi-k2.5-free": { - id: "kimi-k2.5-free", - name: "Kimi K2.5 Free", - family: "kimi-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "minimax-m2.5-free": { - id: "minimax-m2.5-free", - name: "MiniMax M2.5 Free", - family: "minimax-free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "ring-2.6-1t-free": { - id: "ring-2.6-1t-free", - name: "Ring 2.6 1T Free", - family: "ring-1t-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-06", - release_date: "2026-05-08", - last_updated: "2026-05-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262000, output: 66000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "big-pickle": { - id: "big-pickle", - name: "Big Pickle", - family: "big-pickle", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-10-17", - last_updated: "2025-10-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 128000 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen3.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 262144, output: 65536 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - provider: { npm: "@ai-sdk/anthropic" }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "gemini-3-flash": { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - provider: { npm: "@ai-sdk/google" }, - }, - "trinity-large-preview-free": { - id: "trinity-large-preview-free", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-01-28", - last_updated: "2026-01-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - status: "deprecated", - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "glm-5-free": { - id: "glm-5-free", - name: "GLM-5 Free", - family: "glm-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "minimax-m2.1-free": { - id: "minimax-m2.1-free", - name: "MiniMax M2.1 Free", - family: "minimax-free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - provider: { npm: "@ai-sdk/anthropic" }, - }, - "qwen3.6-plus-free": { - id: "qwen3.6-plus-free", - name: "Qwen3.6 Plus Free", - family: "qwen-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1048576, output: 64000 }, - status: "deprecated", - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.1 }, - limit: { context: 204800, output: 131072 }, - status: "deprecated", - }, - "ling-2.6-flash-free": { - id: "ling-2.6-flash-free", - name: "Ling 2.6 Flash Free", - family: "ling-flash-free", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262100, output: 32800 }, - status: "deprecated", - }, - "gemini-3-pro": { - id: "gemini-3-pro", - name: "Gemini 3 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - status: "deprecated", - provider: { npm: "@ai-sdk/google" }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 65536 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "grok-code": { - id: "grok-code", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-20", - last_updated: "2025-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 256000, output: 256000 }, - status: "deprecated", - }, - "mimo-v2-flash-free": { - id: "mimo-v2-flash-free", - name: "MiMo V2 Flash Free", - family: "mimo-flash-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 65536 }, - status: "deprecated", - }, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - family: "gpt-codex-spark", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 128000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "hy3-preview-free": { - id: "hy3-preview-free", - name: "Hy3 preview Free", - family: "hy3-free", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 256000, output: 64000 }, - status: "deprecated", - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.5, cache_read: 0.4 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 64000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "qwen3-coder": { - id: "qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.8 }, - limit: { context: 262144, output: 65536 }, - status: "deprecated", - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen3.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.2, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 262144, output: 65536 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - "mimo-v2-pro-free": { - id: "mimo-v2-pro-free", - name: "MiMo V2 Pro Free", - family: "mimo-pro-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 1048576, output: 64000 }, - status: "deprecated", - }, - "nemotron-3-super-free": { - id: "nemotron-3-super-free", - name: "Nemotron 3 Super Free", - family: "nemotron-free", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 128000 }, - }, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2.5, cache_read: 0.4 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.07, output: 8.5, cache_read: 0.107 }, - limit: { context: 400000, input: 272000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "mimo-v2-omni-free": { - id: "mimo-v2-omni-free", - name: "MiMo V2 Omni Free", - family: "mimo-omni-free", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 64000 }, - status: "deprecated", - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - provider: { npm: "@ai-sdk/openai" }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/anthropic" }, - }, - }, - }, - stepfun: { - id: "stepfun", - env: ["STEPFUN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.stepfun.com/v1", - name: "StepFun", - doc: "https://platform.stepfun.com/docs/zh/overview/concept", - models: { - "step-3.5-flash-2603": { - id: "step-3.5-flash-2603", - name: "Step 3.5 Flash 2603", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.02 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "step-1-32k": { - id: "step-1-32k", - name: "Step 1 (32K)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-01", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.05, output: 9.59, cache_read: 0.41 }, - limit: { context: 32768, input: 32768, output: 32768 }, - }, - "step-3.5-flash": { - id: "step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-29", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.096, output: 0.288, cache_read: 0.019 }, - limit: { context: 256000, input: 256000, output: 256000 }, - }, - "step-2-16k": { - id: "step-2-16k", - name: "Step 2 (16K)", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-01", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5.21, output: 16.44, cache_read: 1.04 }, - limit: { context: 16384, input: 16384, output: 8192 }, - }, - }, - }, - nebius: { - id: "nebius", - env: ["NEBIUS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.tokenfactory.nebius.com/v1", - name: "Nebius Token Factory", - doc: "https://docs.tokenfactory.nebius.com/", - models: { - "NousResearch/Hermes-4-70B": { - id: "NousResearch/Hermes-4-70B", - name: "Hermes-4-70B", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, reasoning: 0.4, cache_read: 0.013, cache_write: 0.16 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "NousResearch/Hermes-4-405B": { - id: "NousResearch/Hermes-4-405B", - name: "Hermes-4-405B", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-30", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, reasoning: 3, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen2.5-VL-72B-Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.75, cache_read: 0.025, cache_write: 0.31 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-397B-A17B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-15", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 262144, input: 250000, output: 8192 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen3-Embedding-8B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - knowledge: "2025-10", - release_date: "2026-01-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, input: 32768, output: 0 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3-30B-A3B-Instruct-2507", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 8192 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen3-32B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507-fast": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507-fast", - name: "Qwen3-235B-A22B-Thinking-2507-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "Qwen/Qwen3.5-397B-A17B-fast": { - id: "Qwen/Qwen3.5-397B-A17B-fast", - name: "Qwen3.5-397B-A17B-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-15", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking-fast": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking-fast", - name: "Qwen3-Next-80B-A3B-Thinking-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.2, cache_read: 0.015, cache_write: 0.1875 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-28", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.2, reasoning: 1.2, cache_read: 0.015, cache_write: 0.18 }, - limit: { context: 128000, input: 120000, output: 16384 }, - }, - "PrimeIntellect/INTELLECT-3": { - id: "PrimeIntellect/INTELLECT-3", - name: "INTELLECT-3", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-25", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.02, cache_write: 0.25 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-03-01", - last_updated: "2026-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3.2, cache_read: 0.1, cache_write: 1 }, - limit: { context: 200000, input: 200000, output: 16384 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-12-05", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.4, cache_read: 0.013, cache_write: 0.16 }, - limit: { context: 128000, input: 120000, output: 8192 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "Meta-Llama-3.1-8B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-07-23", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06, cache_read: 0.002, cache_write: 0.025 }, - limit: { context: 128000, input: 120000, output: 4096 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron-3-Super-120B-A12B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, input: 256000, output: 32768 }, - }, - "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1": { - id: "nvidia/Llama-3_1-Nemotron-Ultra-253B-v1", - name: "Llama-3.1-Nemotron-Ultra-253B-v1", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-15", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8, cache_read: 0.06, cache_write: 0.75 }, - limit: { context: 128000, input: 120000, output: 4096 }, - }, - "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B": { - id: "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B", - name: "Nemotron-3-Nano-30B-A3B", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24, cache_read: 0.006, cache_write: 0.075 }, - limit: { context: 32000, input: 30000, output: 4096 }, - }, - "nvidia/Nemotron-3-Nano-Omni": { - id: "nvidia/Nemotron-3-Nano-Omni", - name: "Nemotron-3-Nano-Omni", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24, cache_read: 0.006, cache_write: 0.075 }, - limit: { context: 65536, input: 60000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.2-fast": { - id: "deepseek-ai/DeepSeek-V3.2-fast", - name: "DeepSeek-V3.2-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2, cache_read: 0.04, cache_write: 0.5 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek-V3.2", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45, reasoning: 0.45, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 163000, input: 160000, output: 16384 }, - }, - "openai/gpt-oss-120b-fast": { - id: "openai/gpt-oss-120b-fast", - name: "gpt-oss-120b-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-10", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-01-10", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6, reasoning: 0.6, cache_read: 0.015, cache_write: 0.18 }, - limit: { context: 128000, input: 124000, output: 8192 }, - }, - "google/gemma-2-2b-it": { - id: "google/gemma-2-2b-it", - name: "Gemma-2-2b-it", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-07-31", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.06, cache_read: 0.002, cache_write: 0.025 }, - limit: { context: 8192, input: 8000, output: 4096 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma-3-27b-it", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2026-01-20", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01, cache_write: 0.125 }, - limit: { context: 110000, input: 100000, output: 8192 }, - }, - "moonshotai/Kimi-K2.5-fast": { - id: "moonshotai/Kimi-K2.5-fast", - name: "Kimi-K2.5-fast", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-15", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.5, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-12-15", - last_updated: "2026-02-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.5, reasoning: 2.5, cache_read: 0.05, cache_write: 0.625 }, - limit: { context: 256000, input: 256000, output: 8192 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 196608, input: 190000, output: 8192 }, - }, - "MiniMaxAI/MiniMax-M2.5-fast": { - id: "MiniMaxAI/MiniMax-M2.5-fast", - name: "MiniMax-M2.5-fast", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2026-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 8000, input: 7000, output: 8192 }, - }, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.75, output: 3.5, cache_read: 0.15 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - poe: { - id: "poe", - env: ["POE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.poe.com/v1", - name: "Poe", - doc: "https://creator.poe.com/docs/external-applications/openai-compatible-api", - models: { - "topazlabs-co/topazlabs": { - id: "topazlabs-co/topazlabs", - name: "TopazLabs", - family: "topazlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 204, output: 0 }, - }, - "novita/kimi-k2.5": { - id: "novita/kimi-k2.5", - name: "Kimi-K2.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 128000, output: 262144 }, - }, - "novita/glm-4.7": { - id: "novita/glm-4.7", - name: "glm-4.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - status: "deprecated", - }, - "novita/glm-5": { - id: "novita/glm-5", - name: "GLM-5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 205000, output: 131072 }, - }, - "novita/minimax-m2.1": { - id: "novita/minimax-m2.1", - name: "minimax-m2.1", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-26", - last_updated: "2025-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/glm-4.6": { - id: "novita/glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "novita/kimi-k2.6": { - id: "novita/kimi-k2.6", - name: "Kimi-K2.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-20", - last_updated: "2026-05-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.96, output: 4.04, cache_read: 0.16 }, - limit: { context: 262144, input: 262144, output: 262144 }, - }, - "novita/glm-4.6v": { - id: "novita/glm-4.6v", - name: "glm-4.6v", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 131000, output: 32768 }, - }, - "novita/deepseek-v3.2": { - id: "novita/deepseek-v3.2", - name: "DeepSeek-V3.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4, cache_read: 0.13 }, - limit: { context: 128000, output: 0 }, - }, - "novita/glm-4.7-flash": { - id: "novita/glm-4.7-flash", - name: "glm-4.7-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 200000, output: 65500 }, - }, - "novita/glm-4.7-n": { - id: "novita/glm-4.7-n", - name: "glm-4.7-n", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 205000, output: 131072 }, - }, - "novita/kimi-k2-thinking": { - id: "novita/kimi-k2-thinking", - name: "kimi-k2-thinking", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 0 }, - }, - "fireworks-ai/kimi-k2.5-fw": { - id: "fireworks-ai/kimi-k2.5-fw", - name: "Kimi-K2.5-FW", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, input: 245760, output: 16384 }, - }, - "empiriolabs/deepseek-v4-pro-el": { - id: "empiriolabs/deepseek-v4-pro-el", - name: "DeepSeek-V4-Pro-EL", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-04-24", - last_updated: "2026-05-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.67, output: 3.33 }, - limit: { context: 1000000, input: 1000000, output: 384000 }, - }, - "empiriolabs/deepseek-v4-flash-el": { - id: "empiriolabs/deepseek-v4-flash-el", - name: "DeepSeek-V4-Flash-EL", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-04-24", - last_updated: "2026-05-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28 }, - limit: { context: 1000000, input: 1000000, output: 384000 }, - }, - "elevenlabs/elevenlabs-v2.5-turbo": { - id: "elevenlabs/elevenlabs-v2.5-turbo", - name: "ElevenLabs-v2.5-Turbo", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-28", - last_updated: "2024-10-28", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "elevenlabs/elevenlabs-v3": { - id: "elevenlabs/elevenlabs-v3", - name: "ElevenLabs-v3", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "elevenlabs/elevenlabs-music": { - id: "elevenlabs/elevenlabs-music", - name: "ElevenLabs-Music", - family: "elevenlabs", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-29", - last_updated: "2025-08-29", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 2000, output: 0 }, - }, - "cerebras/gpt-oss-120b-cs": { - id: "cerebras/gpt-oss-120b-cs", - name: "GPT-OSS-120B-CS", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 0.75 }, - limit: { context: 128000, output: 0 }, - }, - "cerebras/llama-3.1-8b-cs": { - id: "cerebras/llama-3.1-8b-cs", - name: "Llama-3.1-8B-CS", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-13", - last_updated: "2025-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 0 }, - }, - "cerebras/qwen3-32b-cs": { - id: "cerebras/qwen3-32b-cs", - name: "qwen3-32b-cs", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-15", - last_updated: "2025-05-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - status: "deprecated", - }, - "cerebras/qwen3-235b-2507-cs": { - id: "cerebras/qwen3-235b-2507-cs", - name: "qwen3-235b-2507-cs", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-06", - last_updated: "2025-08-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - status: "deprecated", - }, - "cerebras/llama-3.3-70b-cs": { - id: "cerebras/llama-3.3-70b-cs", - name: "llama-3.3-70b-cs", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-13", - last_updated: "2025-05-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - status: "deprecated", - }, - "stabilityai/stablediffusionxl": { - id: "stabilityai/stablediffusionxl", - name: "StableDiffusionXL", - family: "stable-diffusion", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-07-09", - last_updated: "2023-07-09", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 200, output: 0 }, - }, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-22", - last_updated: "2025-08-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 128000 }, - }, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok-4-Fast-Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok-4.1-Fast-Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok-4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 128000 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok-4.20-Multi-Agent", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2 }, - limit: { context: 128000, output: 0 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-11", - last_updated: "2025-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok-4-Fast-Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok-4.1-Fast-Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 2000000, output: 30000 }, - }, - "runwayml/runway": { - id: "runwayml/runway", - name: "Runway", - family: "runway", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-11", - last_updated: "2024-10-11", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 256, output: 0 }, - }, - "runwayml/runway-gen-4-turbo": { - id: "runwayml/runway-gen-4-turbo", - name: "Runway-Gen-4-Turbo", - family: "runway", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-09", - last_updated: "2025-05-09", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 256, output: 0 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/sora-2-pro": { - id: "openai/sora-2-pro", - name: "Sora-2-Pro", - family: "sora", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/chatgpt-4o-latest": { - id: "openai/chatgpt-4o-latest", - name: "ChatGPT-4o-Latest", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-14", - last_updated: "2024-08-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 4.5, output: 14 }, - limit: { context: 128000, output: 8192 }, - status: "deprecated", - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5-Chat", - family: "gpt-codex", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 19, output: 150 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-aug": { - id: "openai/gpt-4o-aug", - name: "GPT-4o-Aug", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-11-21", - last_updated: "2024-11-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 9, cache_read: 1.1 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-image-2": { - id: "openai/gpt-image-2", - name: "GPT-Image-2", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 5.0505, output: 32.3232, cache_read: 1.2626 }, - limit: { context: 0, output: 0 }, - }, - "openai/gpt-4-classic-0314": { - id: "openai/gpt-4-classic-0314", - name: "GPT-4-Classic-0314", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-26", - last_updated: "2024-08-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 27, output: 54 }, - limit: { context: 8192, output: 4096 }, - status: "deprecated", - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5-mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 1.8, cache_read: 0.022 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5-nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.045, output: 0.36, cache_read: 0.0045 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-10", - last_updated: "2026-02-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4-Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-13", - last_updated: "2023-09-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 9, output: 27 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 18, output: 72 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini-high": { - id: "openai/o3-mini-high", - name: "o3-mini-high", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.54, cache_read: 0.068 }, - limit: { context: 124096, output: 4096 }, - }, - "openai/o4-mini-deep-research": { - id: "openai/o4-mini-deep-research", - name: "o4-mini-deep-research", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT-5.4-Mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-12", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.68, output: 4, cache_read: 0.068 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/dall-e-3": { - id: "openai/dall-e-3", - name: "DALL-E-3", - family: "dall-e", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 800, output: 0 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4, cache_read: 0.25 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT-5.4-Nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 1.1, cache_read: 0.018 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-image-1": { - id: "openai/gpt-image-1", - name: "GPT-Image-1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-31", - last_updated: "2025-03-31", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 1.8, cache_read: 0.022 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-image-1-mini": { - id: "openai/gpt-image-1-mini", - name: "GPT-Image-1-Mini", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2024-12-18", - last_updated: "2024-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 14, output: 54 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 27, output: 160 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5-Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-13", - last_updated: "2023-09-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 1.4 }, - limit: { context: 16384, output: 2048 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 9, output: 36, cache_read: 2.2 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.99, output: 4 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1-pro": { - id: "openai/o1-pro", - name: "o1-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-03-19", - last_updated: "2025-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 140, output: 540 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-search": { - id: "openai/gpt-4o-search", - name: "GPT-4o-Search", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 9 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "pdf"], output: ["image"] }, - open_weights: false, - cost: { input: 2.2, output: 14, cache_read: 0.22 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5.3-codex-spark": { - id: "openai/gpt-5.3-codex-spark", - name: "GPT-5.3-Codex-Spark", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-03-04", - last_updated: "2026-03-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-3.5-turbo-raw": { - id: "openai/gpt-3.5-turbo-raw", - name: "GPT-3.5-Turbo-Raw", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-27", - last_updated: "2023-09-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 1.4 }, - limit: { context: 4524, output: 2048 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.36, cache_read: 0.022 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5-Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 14, output: 110 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/sora-2": { - id: "openai/sora-2", - name: "Sora-2", - family: "sora", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-instant": { - id: "openai/gpt-5.2-instant", - name: "GPT-5.2-Instant", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4o-mini-search": { - id: "openai/gpt-4o-mini-search", - name: "GPT-4o-mini-Search", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.54 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-image-1.5": { - id: "openai/gpt-image-1.5", - name: "gpt-image-1.5", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 128000, output: 0 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "GPT-3.5-Turbo-Instruct", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2023-09-20", - last_updated: "2023-09-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 1.8 }, - limit: { context: 3500, output: 1024 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.8, output: 7.2, cache_read: 0.45 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1-Instant", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.36, output: 1.4, cache_read: 0.09 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4-classic": { - id: "openai/gpt-4-classic", - name: "GPT-4-Classic", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-03-25", - last_updated: "2024-03-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 27, output: 54 }, - limit: { context: 8192, output: 4096 }, - status: "deprecated", - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-12", - last_updated: "2025-11-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.3-instant": { - id: "openai/gpt-5.3-instant", - name: "GPT-5.3-Instant", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 13, cache_read: 0.16 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "google/veo-3-fast": { - id: "google/veo-3-fast", - name: "Veo-3-Fast", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-13", - last_updated: "2025-10-13", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/veo-3.1-fast": { - id: "google/veo-3.1-fast", - name: "Veo-3.1-Fast", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-3.1-pro": { - id: "google/gemini-3.1-pro", - name: "Gemini-3.1-Pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/imagen-3-fast": { - id: "google/imagen-3-fast", - name: "Imagen-3-Fast", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-17", - last_updated: "2024-10-17", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.0-flash": { - id: "google/gemini-2.0-flash", - name: "Gemini-2.0-Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.42 }, - limit: { context: 990000, output: 8192 }, - }, - "google/gemini-deep-research": { - id: "google/gemini-deep-research", - name: "gemini-deep-research", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 9.6 }, - limit: { context: 1048576, output: 0 }, - status: "deprecated", - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini-2.5-Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.87, output: 7, cache_read: 0.087 }, - limit: { context: 1065535, output: 65535 }, - }, - "google/imagen-3": { - id: "google/imagen-3", - name: "Imagen-3", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-15", - last_updated: "2024-10-15", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/nano-banana": { - id: "google/nano-banana", - name: "Nano-Banana", - family: "nano-banana", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.21, output: 1.8, cache_read: 0.021 }, - limit: { context: 65536, output: 0 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini-2.5-Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-04-26", - last_updated: "2025-04-26", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 1.8, cache_read: 0.021 }, - limit: { context: 1065535, output: 65535 }, - }, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Gemini-3.1-Flash-Lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini-3-Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-07", - last_updated: "2025-10-07", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, cache_read: 0.04 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/veo-3.1": { - id: "google/veo-3.1", - name: "Veo-3.1", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/lyria": { - id: "google/lyria", - name: "Lyria", - family: "lyria", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-04", - last_updated: "2025-06-04", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "google/imagen-4-ultra": { - id: "google/imagen-4-ultra", - name: "Imagen-4-Ultra", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-24", - last_updated: "2025-05-24", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/nano-banana-pro": { - id: "google/nano-banana-pro", - name: "Nano-Banana-Pro", - family: "nano-banana", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 65536, output: 0 }, - }, - "google/gemini-3-pro": { - id: "google/gemini-3-pro", - name: "Gemini-3-Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-22", - last_updated: "2025-10-22", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 9.6, cache_read: 0.16 }, - limit: { context: 1048576, output: 65536 }, - status: "deprecated", - }, - "google/imagen-4-fast": { - id: "google/imagen-4-fast", - name: "Imagen-4-Fast", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/veo-3": { - id: "google/veo-3", - name: "Veo-3", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini-2.5-Flash-Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-06-19", - last_updated: "2025-06-19", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 1024000, output: 64000 }, - }, - "google/imagen-4": { - id: "google/imagen-4", - name: "Imagen-4", - family: "imagen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemma-4-31b": { - id: "google/gemma-4-31b", - name: "Gemma-4-31B", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 8192 }, - }, - "google/gemini-2.0-flash-lite": { - id: "google/gemini-2.0-flash-lite", - name: "Gemini-2.0-Flash-Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-05", - last_updated: "2025-02-05", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.052, output: 0.21 }, - limit: { context: 990000, output: 8192 }, - }, - "google/veo-2": { - id: "google/veo-2", - name: "Veo-2", - family: "veo", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-12-02", - last_updated: "2024-12-02", - modalities: { input: ["text"], output: ["video"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "lumalabs/ray2": { - id: "lumalabs/ray2", - name: "Ray2", - family: "ray", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-20", - last_updated: "2025-02-20", - modalities: { input: ["text", "image"], output: ["video"] }, - open_weights: false, - limit: { context: 5000, output: 0 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude-Opus-4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 13, output: 64, cache_read: 1.3, cache_write: 16 }, - limit: { context: 196608, output: 32000 }, - }, - "anthropic/claude-sonnet-3.5": { - id: "anthropic/claude-sonnet-3.5", - name: "Claude-Sonnet-3.5", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-06-05", - last_updated: "2024-06-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 189096, output: 8192 }, - status: "deprecated", - }, - "anthropic/claude-haiku-3": { - id: "anthropic/claude-haiku-3", - name: "Claude-Haiku-3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-03-09", - last_updated: "2024-03-09", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 1.1, cache_read: 0.021, cache_write: 0.26 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude-Opus-4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.3, output: 21, cache_read: 0.43, cache_write: 5.3 }, - limit: { context: 983040, output: 128000 }, - }, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude-Opus-4.7", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-15", - last_updated: "2026-04-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.3, output: 21, cache_read: 0.43, cache_write: 5.4 }, - limit: { context: 1048576, output: 128000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude-Sonnet-4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 64000 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude-Sonnet-4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 32768 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude-Opus-4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-21", - last_updated: "2025-11-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 4.3, output: 21, cache_read: 0.43, cache_write: 5.3 }, - limit: { context: 196608, output: 64000 }, - }, - "anthropic/claude-sonnet-3.7": { - id: "anthropic/claude-sonnet-3.7", - name: "Claude-Sonnet-3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 196608, output: 128000 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude-Opus-4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-05-21", - last_updated: "2025-05-21", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 13, output: 64, cache_read: 1.3, cache_write: 16 }, - limit: { context: 192512, output: 28672 }, - }, - "anthropic/claude-haiku-3.5": { - id: "anthropic/claude-haiku-3.5", - name: "Claude-Haiku-3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.68, output: 3.4, cache_read: 0.068, cache_write: 0.85 }, - limit: { context: 189096, output: 8192 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude-Haiku-4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.85, output: 4.3, cache_read: 0.085, cache_write: 1.1 }, - limit: { context: 192000, output: 64000 }, - }, - "anthropic/claude-sonnet-3.5-june": { - id: "anthropic/claude-sonnet-3.5-june", - name: "Claude-Sonnet-3.5-June", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-11-18", - last_updated: "2024-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 189096, output: 8192 }, - status: "deprecated", - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude-Sonnet-4.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.6, output: 13, cache_read: 0.26, cache_write: 3.2 }, - limit: { context: 983040, output: 128000 }, - }, - "ideogramai/ideogram": { - id: "ideogramai/ideogram", - name: "Ideogram", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-04-03", - last_updated: "2024-04-03", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2": { - id: "ideogramai/ideogram-v2", - name: "Ideogram-v2", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-21", - last_updated: "2024-08-21", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2a-turbo": { - id: "ideogramai/ideogram-v2a-turbo", - name: "Ideogram-v2a-Turbo", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "ideogramai/ideogram-v2a": { - id: "ideogramai/ideogram-v2a", - name: "Ideogram-v2a", - family: "ideogram", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 150, output: 0 }, - }, - "trytako/tako": { - id: "trytako/tako", - name: "Tako", - family: "tako", - attachment: true, - reasoning: false, - tool_call: true, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 2048, output: 0 }, - }, - "poetools/claude-code": { - id: "poetools/claude-code", - name: "claude-code", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2025-11-27", - last_updated: "2025-11-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 4.5455, output: 27.2727, cache_read: 0.4545 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT-5.5-Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 27.2727, output: 163.6364 }, - limit: { context: 400000, output: 128000 }, - }, - }, - }, - helicone: { - id: "helicone", - env: ["HELICONE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://ai-gateway.helicone.ai/v1", - name: "Helicone", - doc: "https://helicone.ai/models", - models: { - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 40 }, - limit: { context: 128000, output: 16400 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "xAI Grok 4.1 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Google Gemma 2", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-25", - last_updated: "2024-06-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.03 }, - limit: { context: 8192, output: 8192 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Meta Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.39 }, - limit: { context: 128000, output: 16400 }, - }, - "llama-4-scout": { - id: "llama-4-scout", - name: "Meta Llama 4 Scout 17B 16E", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 131072, output: 8192 }, - }, - "chatgpt-4o-latest": { - id: "chatgpt-4o-latest", - name: "OpenAI ChatGPT-4o", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-14", - last_updated: "2024-08-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 20, cache_read: 2.5 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-3.5-sonnet-v2": { - id: "claude-3.5-sonnet-v2", - name: "Anthropic: Claude 3.5 Sonnet v2", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "hermes-2-pro-llama-3-8b": { - id: "hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-3.7-sonnet": { - id: "claude-3.7-sonnet", - name: "Anthropic: Claude 3.7 Sonnet", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-02", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "llama-prompt-guard-2-22m": { - id: "llama-prompt-guard-2-22m", - name: "Meta Llama Prompt Guard 2 22M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 512, output: 2 }, - }, - "o1-mini": { - id: "o1-mini", - name: "OpenAI: o1-mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "gpt-4.1-mini-2025-04-14": { - id: "gpt-4.1-mini-2025-04-14", - name: "OpenAI GPT-4.1 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 1.5999999999999999, cache_read: 0.09999999999999999 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.13 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 0.59 }, - limit: { context: 131072, output: 40960 }, - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Meta Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.7899999999999999 }, - limit: { context: 131072, output: 32678 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "OpenAI GPT-5 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.024999999999999998 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "OpenAI GPT-5 Nano", - family: "gpt-nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.39999999999999997, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Google Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.19999999999999998 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Anthropic: Claude 3 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-03-07", - last_updated: "2024-03-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "llama-4-maverick": { - id: "llama-4-maverick", - name: "Meta Llama 4 Maverick 17B 128E", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 8192 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Anthropic: Claude Sonnet 4.5 (20250929)", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Google Gemini 2.5 Pro", - family: "gemini-pro", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.3125, cache_write: 1.25 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-4.5-opus": { - id: "claude-4.5-opus", - name: "Anthropic: Claude Opus 4.5", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "xAI Grok 4.1 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-17", - last_updated: "2025-11-17", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 30000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Perplexity Sonar Pro", - family: "sonar-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 4096 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral-Large", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "o3-pro": { - id: "o3-pro", - name: "OpenAI o3 Pro", - family: "o-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Anthropic: Claude Opus 4.1", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "OpenAI GPT-4o-mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.075 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-4.5-haiku": { - id: "claude-4.5-haiku", - name: "Anthropic: Claude 4.5 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.09999999999999999, cache_write: 1.25 }, - limit: { context: 200000, output: 8192 }, - }, - "kimi-k2-0711": { - id: "kimi-k2-0711", - name: "Kimi K2 (07/11)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5700000000000001, output: 2.3 }, - limit: { context: 131072, output: 16384 }, - }, - "o4-mini": { - id: "o4-mini", - name: "OpenAI o4 Mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.275 }, - limit: { context: 200000, output: 100000 }, - }, - "sonar-deep-research": { - id: "sonar-deep-research", - name: "Perplexity Sonar Deep Research", - family: "sonar-deep-research", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 4096 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Google Gemma 3 12B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.09999999999999999 }, - limit: { context: 131072, output: 8192 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Google Gemini 2.5 Flash", - family: "gemini-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.3 }, - limit: { context: 1048576, output: 65535 }, - }, - "deepseek-tng-r1t2-chimera": { - id: "deepseek-tng-r1t2-chimera", - name: "DeepSeek TNG R1T2 Chimera", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-02", - last_updated: "2025-07-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 130000, output: 163840 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "OpenAI: GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.024999999999999998 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Anthropic: Claude Sonnet 4", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "xAI Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-25", - last_updated: "2024-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "OpenAI GPT-5.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "deepseek-reasoner": { - id: "deepseek-reasoner", - name: "DeepSeek Reasoner", - family: "deepseek-thinking", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68, cache_read: 0.07 }, - limit: { context: 128000, output: 64000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "xAI: Grok 4 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - o1: { - id: "o1", - name: "OpenAI: o1", - family: "o", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Meta Llama 3.1 8B Instant", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.08 }, - limit: { context: 131072, output: 32678 }, - }, - "o3-mini": { - id: "o3-mini", - name: "OpenAI o3 Mini", - family: "o-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2023-10", - release_date: "2023-10-01", - last_updated: "2023-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - sonar: { - id: "sonar", - name: "Perplexity Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127000, output: 4096 }, - }, - "kimi-k2-0905": { - id: "kimi-k2-0905", - name: "Kimi K2 (09/05)", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2, cache_read: 0.39999999999999997 }, - limit: { context: 262144, output: 16384 }, - }, - "mistral-small": { - id: "mistral-small", - name: "Mistral Small", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-02", - release_date: "2024-02-26", - last_updated: "2024-02-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 75, output: 200 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-30b-a3b": { - id: "qwen3-30b-a3b", - name: "Qwen3 30B A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.29 }, - limit: { context: 41000, output: 41000 }, - }, - "grok-4": { - id: "grok-4", - name: "xAI Grok 4", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-09", - last_updated: "2024-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-235b-a22b-thinking": { - id: "qwen3-235b-a22b-thinking", - name: "Qwen3 235B A22B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.9000000000000004 }, - limit: { context: 262144, output: 81920 }, - }, - "qwen2.5-coder-7b-fast": { - id: "qwen2.5-coder-7b-fast", - name: "Qwen2.5 Coder 7B fast", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-15", - last_updated: "2024-09-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.09 }, - limit: { context: 32000, output: 8192 }, - }, - "llama-3.1-8b-instruct-turbo": { - id: "llama-3.1-8b-instruct-turbo", - name: "Meta Llama 3.1 8B Instruct Turbo", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.03 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 16384 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "Zai GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.44999999999999996, output: 1.5 }, - limit: { context: 204800, output: 131072 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "OpenAI: GPT-5 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Anthropic: Claude Opus 4.1 (20250805)", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "OpenAI GPT-5.1 Chat", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Anthropic: Claude 4.5 Haiku (20251001)", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.09999999999999999, cache_write: 1.25 }, - limit: { context: 200000, output: 8192 }, - }, - "sonar-reasoning": { - id: "sonar-reasoning", - name: "Perplexity Sonar Reasoning", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 127000, output: 4096 }, - }, - "claude-opus-4": { - id: "claude-opus-4", - name: "Anthropic: Claude Opus 4", - family: "claude-opus", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-14", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "llama-prompt-guard-2-86m": { - id: "llama-prompt-guard-2-86m", - name: "Meta Llama Prompt Guard 2 86M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 512, output: 2 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "OpenAI GPT-4.1 Nano", - family: "gpt-nano", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09999999999999999, output: 0.39999999999999997, cache_read: 0.024999999999999998 }, - limit: { context: 1047576, output: 32768 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09999999999999999, output: 0.3 }, - limit: { context: 262144, output: 262144 }, - }, - "claude-3.5-haiku": { - id: "claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7999999999999999, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "xAI Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 131072 }, - }, - o3: { - id: "o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 163840, output: 65536 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "OpenAI GPT-OSS 20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.19999999999999998 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "OpenAI: GPT-5 Pro", - family: "gpt-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 128000, output: 32768 }, - }, - "llama-guard-4": { - id: "llama-guard-4", - name: "Meta Llama Guard 4 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.21 }, - limit: { context: 131072, output: 1024 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "OpenAI GPT-4o", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-05", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-235b-a22b-instruct": { - id: "qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Google Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cache_read: 0.024999999999999998, - cache_write: 0.09999999999999999, - }, - limit: { context: 1048576, output: 65535 }, - }, - "qwen3-coder": { - id: "qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.22, output: 0.95 }, - limit: { context: 262144, output: 16384 }, - }, - "gpt-5": { - id: "gpt-5", - name: "OpenAI GPT-5", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "ernie-4.5-21b-a3b-thinking": { - id: "ernie-4.5-21b-a3b-thinking", - name: "Baidu Ernie 4.5 21B A3B Thinking", - family: "ernie", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 128000, output: 8000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "OpenAI GPT-OSS 120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "OpenAI GPT-5 Chat Latest", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2024-09", - release_date: "2024-09-30", - last_updated: "2024-09-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-4.5-sonnet": { - id: "claude-4.5-sonnet", - name: "Anthropic: Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.30000000000000004, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68, cache_read: 0.07 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Meta Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0.049999999999999996 }, - limit: { context: 16384, output: 16384 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "OpenAI GPT-4.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.48, output: 2 }, - limit: { context: 256000, output: 262144 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "OpenAI GPT-4.1 Mini", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 1.5999999999999999, cache_read: 0.09999999999999999 }, - limit: { context: 1047576, output: 32768 }, - }, - "deepseek-v3.1-terminus": { - id: "deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1, cache_read: 0.21600000000000003 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "OpenAI: GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: false, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.12500000000000003 }, - limit: { context: 400000, output: 128000 }, - }, - "grok-3": { - id: "grok-3", - name: "xAI Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 131072 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "xAI Grok 4 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 0.5, cache_read: 0.049999999999999996 }, - limit: { context: 2000000, output: 2000000 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Perplexity Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-27", - last_updated: "2025-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 4096 }, - }, - }, - }, - "ollama-cloud": { - id: "ollama-cloud", - env: ["OLLAMA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://ollama.com/v1", - name: "Ollama Cloud", - doc: "https://docs.ollama.com/cloud", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "minimax-m2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 196608, output: 196608 }, - }, - "gpt-oss:20b": { - id: "gpt-oss:20b", - name: "gpt-oss:20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "kimi-k2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "glm-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-22", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "gemma4:31b": { - id: "gemma4:31b", - name: "gemma4:31b", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "gpt-oss:120b": { - id: "gpt-oss:120b", - name: "gpt-oss:120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 32768 }, - }, - "qwen3.5:397b": { - id: "qwen3.5:397b", - name: "qwen3.5:397b", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - release_date: "2026-02-15", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "deepseek-v3.1:671b": { - id: "deepseek-v3.1:671b", - name: "deepseek-v3.1:671b", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-21", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 163840 }, - }, - "glm-5": { - id: "glm-5", - name: "glm-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "qwen3-vl:235b-instruct": { - id: "qwen3-vl:235b-instruct", - name: "qwen3-vl:235b-instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-09-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 131072 }, - }, - "gemma3:4b": { - id: "gemma3:4b", - name: "gemma3:4b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "gemini-3-flash-preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 65536 }, - }, - "ministral-3:14b": { - id: "ministral-3:14b", - name: "ministral-3:14b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "minimax-m2", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-10-23", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 128000 }, - }, - "qwen3-next:80b": { - id: "qwen3-next:80b", - name: "qwen3-next:80b", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-09-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-vl:235b": { - id: "qwen3-vl:235b", - name: "qwen3-vl:235b", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2025-09-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 32768 }, - }, - "rnj-1:8b": { - id: "rnj-1:8b", - name: "rnj-1:8b", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-06", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 32768, output: 4096 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "minimax-m2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-23", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "glm-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-03-27", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "mistral-large-3:675b": { - id: "mistral-large-3:675b", - name: "mistral-large-3:675b", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-12-02", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "ministral-3:8b": { - id: "ministral-3:8b", - name: "ministral-3:8b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "gemma3:12b": { - id: "gemma3:12b", - name: "gemma3:12b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "qwen3-coder:480b": { - id: "qwen3-coder:480b", - name: "qwen3-coder:480b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-22", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "kimi-k2.6:cloud": { - id: "kimi-k2.6:cloud", - name: "kimi-k2.6:cloud", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "nemotron-3-nano:30b": { - id: "nemotron-3-nano:30b", - name: "nemotron-3-nano:30b", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-12-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 131072 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "deepseek-v4-flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 1048576 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "glm-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-09-29", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 202752, output: 131072 }, - }, - "ministral-3:3b": { - id: "ministral-3:3b", - name: "ministral-3:3b", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2024-10-22", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 128000 }, - }, - "gemma3:27b": { - id: "gemma3:27b", - name: "gemma3:27b", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - release_date: "2025-07-27", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "devstral-2:123b": { - id: "devstral-2:123b", - name: "devstral-2:123b", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-09", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "cogito-2.1:671b": { - id: "cogito-2.1:671b", - name: "cogito-2.1:671b", - family: "cogito", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-11-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 32000 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "qwen3-coder-next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2026-02-02", - last_updated: "2026-02-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "nemotron-3-super": { - id: "nemotron-3-super", - name: "nemotron-3-super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-03-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 65536 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "deepseek-v4-pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 1048576, output: 1048576 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "minimax-m2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - knowledge: "2025-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 204800, output: 131072 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "deepseek-v3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-06-15", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 65536 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "kimi-k2-thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "devstral-small-2:24b": { - id: "devstral-small-2:24b", - name: "devstral-small-2:24b", - family: "devstral", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-12-09", - last_updated: "2026-01-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2:1t": { - id: "kimi-k2:1t", - name: "kimi-k2:1t", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "zai-coding-plan": { - id: "zai-coding-plan", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.z.ai/api/coding/paas/v4", - name: "Z.AI Coding Plan", - doc: "https://docs.z.ai/devpack/overview", - models: { - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - }, - }, - "amazon-bedrock": { - id: "amazon-bedrock", - env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_BEARER_TOKEN_BEDROCK"], - npm: "@ai-sdk/amazon-bedrock", - name: "Amazon Bedrock", - doc: "https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html", - models: { - "openai.gpt-oss-safeguard-120b": { - id: "openai.gpt-oss-safeguard-120b", - name: "GPT OSS Safeguard 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-nano-3-30b": { - id: "nvidia.nemotron-nano-3-30b", - name: "NVIDIA Nemotron Nano 3 30B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-super-3-120b": { - id: "nvidia.nemotron-super-3-120b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.65 }, - limit: { context: 262144, output: 131072 }, - }, - "writer.palmyra-x5-v1:0": { - id: "writer.palmyra-x5-v1:0", - name: "Palmyra X5", - family: "palmyra", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 6 }, - limit: { context: 1040000, output: 8192 }, - }, - "mistral.ministral-3-8b-instruct": { - id: "mistral.ministral-3-8b-instruct", - name: "Ministral 3 8B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 4096 }, - }, - "au.anthropic.claude-opus-4-6-v1": { - id: "au.anthropic.claude-opus-4-6-v1", - name: "AU Anthropic Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 82.5, cache_read: 1.65, cache_write: 20.625 }, - limit: { context: 1000000, output: 128000 }, - }, - "mistral.ministral-3-3b-instruct": { - id: "mistral.ministral-3-3b-instruct", - name: "Ministral 3 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 256000, output: 8192 }, - }, - "anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.devstral-2-123b": { - id: "mistral.devstral-2-123b", - name: "Devstral 2 123B", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 256000, output: 8192 }, - }, - "global.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "global.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (Global)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.voxtral-small-24b-2507": { - id: "mistral.voxtral-small-24b-2507", - name: "Voxtral Small 24B 2507", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-01", - last_updated: "2025-07-01", - modalities: { input: ["text", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.35 }, - limit: { context: 32000, output: 8192 }, - }, - "google.gemma-3-12b-it": { - id: "google.gemma-3-12b-it", - name: "Google Gemma 3 12B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.049999999999999996, output: 0.09999999999999999 }, - limit: { context: 131072, output: 8192 }, - }, - "amazon.nova-pro-v1:0": { - id: "amazon.nova-pro-v1:0", - name: "Nova Pro", - family: "nova-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2, cache_read: 0.2 }, - limit: { context: 300000, output: 8192 }, - }, - "anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "minimax.minimax-m2": { - id: "minimax.minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204608, output: 128000 }, - }, - "global.anthropic.claude-opus-4-7": { - id: "global.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (Global)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "mistral.pixtral-large-2502-v1:0": { - id: "mistral.pixtral-large-2502-v1:0", - name: "Pixtral Large (25.02)", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-08", - last_updated: "2025-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 8192 }, - }, - "meta.llama4-maverick-17b-instruct-v1:0": { - id: "meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.97 }, - limit: { context: 1000000, output: 16384 }, - }, - "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (US)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "us.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (US)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "amazon.nova-micro-v1:0": { - id: "amazon.nova-micro-v1:0", - name: "Nova Micro", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14, cache_read: 0.00875 }, - limit: { context: 128000, output: 8192 }, - }, - "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "global.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (Global)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "openai.gpt-oss-20b-1:0": { - id: "openai.gpt-oss-20b-1:0", - name: "gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "zai.glm-5": { - id: "zai.glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 202752, output: 101376 }, - }, - "qwen.qwen3-32b-v1:0": { - id: "qwen.qwen3-32b-v1:0", - name: "Qwen3 32B (dense)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 16384, output: 16384 }, - }, - "deepseek.v3.2": { - id: "deepseek.v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.62, output: 1.85 }, - limit: { context: 163840, output: 81920 }, - }, - "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (EU)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "zai.glm-4.7-flash": { - id: "zai.glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4 }, - limit: { context: 200000, output: 131072 }, - }, - "us.anthropic.claude-opus-4-7": { - id: "us.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "amazon.nova-2-lite-v1:0": { - id: "amazon.nova-2-lite-v1:0", - name: "Nova 2 Lite", - family: "nova", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.33, output: 2.75 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-opus-4-5-20251101-v1:0": { - id: "anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen.qwen3-coder-480b-a35b-v1:0": { - id: "qwen.qwen3-coder-480b-a35b-v1:0", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.8 }, - limit: { context: 131072, output: 65536 }, - }, - "amazon.nova-lite-v1:0": { - id: "amazon.nova-lite-v1:0", - name: "Nova Lite", - family: "nova-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24, cache_read: 0.015 }, - limit: { context: 300000, output: 8192 }, - }, - "meta.llama3-1-8b-instruct-v1:0": { - id: "meta.llama3-1-8b-instruct-v1:0", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 4096 }, - }, - "anthropic.claude-opus-4-7": { - id: "anthropic.claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "google.gemma-3-27b-it": { - id: "google.gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-27", - last_updated: "2025-07-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.2 }, - limit: { context: 202752, output: 8192 }, - }, - "global.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "global.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (Global)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "google.gemma-3-4b-it": { - id: "google.gemma-3-4b-it", - name: "Gemma 3 4B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.08 }, - limit: { context: 128000, output: 4096 }, - }, - "meta.llama4-scout-17b-instruct-v1:0": { - id: "meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 3500000, output: 16384 }, - }, - "deepseek.v3-v1:0": { - id: "deepseek.v3-v1:0", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 163840, output: 81920 }, - }, - "mistral.magistral-small-2509": { - id: "mistral.magistral-small-2509", - name: "Magistral Small 1.2", - family: "magistral", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 40000 }, - }, - "qwen.qwen3-next-80b-a3b": { - id: "qwen.qwen3-next-80b-a3b", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "zai.glm-4.7": { - id: "zai.glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "moonshot.kimi-k2-thinking": { - id: "moonshot.kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 256000, output: 256000 }, - }, - "us.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "us.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "mistral.ministral-3-14b-instruct": { - id: "mistral.ministral-3-14b-instruct", - name: "Ministral 14B 3.0", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek.r1-v1:0": { - id: "deepseek.r1-v1:0", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral.voxtral-mini-3b-2507": { - id: "mistral.voxtral-mini-3b-2507", - name: "Voxtral Mini 3B 2507", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["audio", "text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 4096 }, - }, - "openai.gpt-oss-120b-1:0": { - id: "openai.gpt-oss-120b-1:0", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia.nemotron-nano-12b-v2": { - id: "nvidia.nemotron-nano-12b-v2", - name: "NVIDIA Nemotron Nano 12B v2 VL BF16", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "eu.anthropic.claude-opus-4-7": { - id: "eu.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (EU)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "minimax.minimax-m2.5": { - id: "minimax.minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 98304 }, - }, - "meta.llama3-3-70b-instruct-v1:0": { - id: "meta.llama3-3-70b-instruct-v1:0", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 4096 }, - }, - "meta.llama3-1-70b-instruct-v1:0": { - id: "meta.llama3-1-70b-instruct-v1:0", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 4096 }, - }, - "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (EU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "eu.anthropic.claude-opus-4-5-20251101-v1:0": { - id: "eu.anthropic.claude-opus-4-5-20251101-v1:0", - name: "Claude Opus 4.5 (EU)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "moonshotai.kimi-k2.5": { - id: "moonshotai.kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 256000, output: 256000 }, - }, - "au.anthropic.claude-sonnet-4-6": { - id: "au.anthropic.claude-sonnet-4-6", - name: "AU Anthropic Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.3, output: 16.5, cache_read: 0.33, cache_write: 4.125 }, - limit: { context: 1000000, output: 128000 }, - }, - "openai.gpt-oss-safeguard-20b": { - id: "openai.gpt-oss-safeguard-20b", - name: "GPT OSS Safeguard 20B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.2 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen.qwen3-coder-30b-a3b-v1:0": { - id: "qwen.qwen3-coder-30b-a3b-v1:0", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 131072 }, - }, - "minimax.minimax-m2.1": { - id: "minimax.minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen.qwen3-vl-235b-a22b": { - id: "qwen.qwen3-vl-235b-a22b", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen.qwen3-coder-next": { - id: "qwen.qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.8 }, - limit: { context: 131072, output: 65536 }, - }, - "nvidia.nemotron-nano-9b-v2": { - id: "nvidia.nemotron-nano-9b-v2", - name: "NVIDIA Nemotron Nano 9B v2", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.23 }, - limit: { context: 128000, output: 4096 }, - }, - "mistral.mistral-large-3-675b-instruct": { - id: "mistral.mistral-large-3-675b-instruct", - name: "Mistral Large 3", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 8192 }, - }, - "qwen.qwen3-235b-a22b-2507-v1:0": { - id: "qwen.qwen3-235b-a22b-2507-v1:0", - name: "Qwen3 235B A22B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-18", - last_updated: "2025-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 262144, output: 131072 }, - }, - "writer.palmyra-x4-v1:0": { - id: "writer.palmyra-x4-v1:0", - name: "Palmyra X4", - family: "palmyra", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 122880, output: 8192 }, - }, - "anthropic.claude-opus-4-1-20250805-v1:0": { - id: "anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "us.deepseek.r1-v1:0": { - id: "us.deepseek.r1-v1:0", - name: "DeepSeek-R1 (US)", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 32768 }, - }, - "eu.anthropic.claude-opus-4-6-v1": { - id: "eu.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (EU)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "us.meta.llama4-maverick-17b-instruct-v1:0": { - id: "us.meta.llama4-maverick-17b-instruct-v1:0", - name: "Llama 4 Maverick 17B Instruct (US)", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.97 }, - limit: { context: 1000000, output: 16384 }, - }, - "au.anthropic.claude-haiku-4-5-20251001-v1:0": { - id: "au.anthropic.claude-haiku-4-5-20251001-v1:0", - name: "Claude Haiku 4.5 (AU)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (JP)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic.claude-sonnet-4-6": { - id: "anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "jp.anthropic.claude-sonnet-4-6": { - id: "jp.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (JP)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "global.anthropic.claude-sonnet-4-6": { - id: "global.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (Global)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "us.anthropic.claude-sonnet-4-6": { - id: "us.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (US)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "global.anthropic.claude-opus-4-6-v1": { - id: "global.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (Global)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "us.anthropic.claude-opus-4-6-v1": { - id: "us.anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "us.anthropic.claude-opus-4-1-20250805-v1:0": { - id: "us.anthropic.claude-opus-4-1-20250805-v1:0", - name: "Claude Opus 4.1 (US)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { - id: "au.anthropic.claude-sonnet-4-5-20250929-v1:0", - name: "Claude Sonnet 4.5 (AU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "eu.anthropic.claude-sonnet-4-6": { - id: "eu.anthropic.claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (EU)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "us.meta.llama4-scout-17b-instruct-v1:0": { - id: "us.meta.llama4-scout-17b-instruct-v1:0", - name: "Llama 4 Scout 17B Instruct (US)", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 3500000, output: 16384 }, - }, - "anthropic.claude-opus-4-6-v1": { - id: "anthropic.claude-opus-4-6-v1", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "jp.anthropic.claude-opus-4-7": { - id: "jp.anthropic.claude-opus-4-7", - name: "Claude Opus 4.7 (JP)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - }, - }, - "the-grid-ai": { - id: "the-grid-ai", - env: ["THEGRIDAI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.thegrid.ai/v1", - name: "The Grid AI", - doc: "https://thegrid.ai/docs", - models: { - "text-prime": { - id: "text-prime", - name: "Text Prime", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 30000 }, - status: "beta", - }, - "text-standard": { - id: "text-standard", - name: "Text Standard", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 16000 }, - status: "beta", - }, - "text-max": { - id: "text-max", - name: "Text Max", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 1000000, output: 128000 }, - status: "beta", - }, - }, - }, - baseten: { - id: "baseten", - env: ["BASETEN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://inference.baseten.co/v1", - name: "Baseten", - doc: "https://docs.baseten.co/development/model-apis/overview", - models: { - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 204800, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-09-16", - last_updated: "2025-09-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 200000, output: 200000 }, - }, - "nvidia/Nemotron-120B-A12B": { - id: "nvidia/Nemotron-120B-A12B", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.75 }, - limit: { context: 262144, output: 32678 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 164000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.77, output: 0.77 }, - limit: { context: 164000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-01", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.45 }, - limit: { context: 163800, output: 131100 }, - status: "deprecated", - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 128000, output: 128000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-09-05", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - status: "deprecated", - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-30", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 8192 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204000, output: 204000 }, - }, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.15 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - frogbot: { - id: "frogbot", - env: ["FROGBOT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://app.frogbot.ai/api/v1", - name: "FrogBot", - doc: "https://docs.frogbot.ai", - models: { - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi-K2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 128000 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 128000 }, - }, - "zai-glm-5-1": { - id: "zai-glm-5-1", - name: "Z.AI GLM-5.1", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-20", - last_updated: "2025-02-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 198000, output: 8192 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 128000 }, - }, - "gpt-5-4-nano": { - id: "gpt-5-4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-17", - last_updated: "2025-07-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075 }, - limit: { context: 1048576, output: 65536 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 128000 }, - }, - "gpt-5-5": { - id: "gpt-5-5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 272000, output: 128000 }, - }, - "grok-4-3": { - id: "grok-4-3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-11", - release_date: "2026-04-30", - last_updated: "2026-04-30", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 2.5, cache_read: 0.2 }, - limit: { context: 1000000, output: 128000 }, - }, - "gpt-5-4-mini": { - id: "gpt-5-4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 128000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek v4 Pro", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.74, output: 3.48, cache_read: 0.14 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.2 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen-3-6-plus": { - id: "qwen-3-6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.1 }, - limit: { context: 1000000, output: 64000 }, - }, - "minimax-m2-7": { - id: "minimax-m2-7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 192000, output: 8192 }, - }, - "minimax-m2-5": { - id: "minimax-m2-5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-01-15", - last_updated: "2025-02-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03 }, - limit: { context: 192000, output: 8192 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "gemini-3-1-pro-preview": { - id: "gemini-3-1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-18", - last_updated: "2026-02-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1000000, output: 64000 }, - }, - "kimi-k2-6": { - id: "kimi-k2-6", - name: "Kimi-K2.6", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "1970-01-01", - last_updated: "1970-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 256000, output: 128000 }, - }, - "gpt-5-3-codex": { - id: "gpt-5-3-codex", - name: "GPT-5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - }, - }, - "zhipuai-coding-plan": { - id: "zhipuai-coding-plan", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://open.bigmodel.cn/api/coding/paas/v4", - name: "Zhipu AI Coding Plan", - doc: "https://docs.bigmodel.cn/cn/coding-plan/overview", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5-turbo": { - id: "glm-5-turbo", - name: "GLM-5-Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "alibaba-coding-plan": { - id: "alibaba-coding-plan", - env: ["ALIBABA_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding-intl.dashscope.aliyuncs.com/v1", - name: "Alibaba Coding Plan", - doc: "https://www.alibabacloud.com/help/en/model-studio/coding-plan", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 196608, input: 196601, output: 24576 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - }, - }, - venice: { - id: "venice", - env: ["VENICE_API_KEY"], - npm: "venice-ai-sdk-provider", - name: "Venice AI", - doc: "https://docs.venice.ai", - models: { - "openai-gpt-4o-mini-2024-07-18": { - id: "openai-gpt-4o-mini-2024-07-18", - name: "GPT-4o Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-28", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1875, output: 0.75, cache_read: 0.09375 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-next-80b": { - id: "qwen3-next-80b", - name: "Qwen 3 Next 80b", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.9 }, - limit: { context: 256000, output: 16384 }, - }, - "grok-4-20-multi-agent": { - id: "grok-4-20-multi-agent", - name: "Grok 4.20 Multi-Agent", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.42, - output: 2.83, - cache_read: 0.23, - context_over_200k: { input: 2.83, output: 5.67, cache_read: 0.45 }, - }, - limit: { context: 2000000, output: 128000 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen 3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "z-ai-glm-5v-turbo": { - id: "z-ai-glm-5v-turbo", - name: "GLM 5V Turbo", - family: "glmv", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 5, cache_read: 0.3 }, - limit: { context: 200000, output: 32768 }, - }, - "gemma-4-uncensored": { - id: "gemma-4-uncensored", - name: "Gemma 4 Uncensored", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-13", - last_updated: "2026-04-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1625, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - "grok-41-fast": { - id: "grok-41-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-12-01", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.23, output: 0.57, cache_read: 0.06 }, - limit: { context: 1000000, output: 30000 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.6, output: 18, cache_read: 0.36, cache_write: 4.5 }, - limit: { context: 1000000, output: 64000 }, - }, - "nvidia-nemotron-cascade-2-30b-a3b": { - id: "nvidia-nemotron-cascade-2-30b-a3b", - name: "Nemotron Cascade 2 30B A3B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.8 }, - limit: { context: 256000, output: 32768 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-19", - last_updated: "2026-03-12", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7, output: 3.75, cache_read: 0.07 }, - limit: { context: 256000, output: 65536 }, - }, - "grok-4-20": { - id: "grok-4-20", - name: "Grok 4.20", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-12", - last_updated: "2026-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.42, - output: 2.83, - cache_read: 0.23, - context_over_200k: { input: 2.83, output: 5.67, cache_read: 0.45 }, - }, - limit: { context: 2000000, output: 128000 }, - }, - "google-gemma-4-26b-a4b-it": { - id: "google-gemma-4-26b-a4b-it", - name: "Google Gemma 4 26B A4B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-12", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1625, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30, cache_read: 0.6, cache_write: 7.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "qwen3-coder-480b-a35b-instruct-turbo": { - id: "qwen3-coder-480b-a35b-instruct-turbo", - name: "Qwen 3 Coder 480B Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-02-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.5, cache_read: 0.04 }, - limit: { context: 256000, output: 65536 }, - }, - "qwen3-5-397b-a17b": { - id: "qwen3-5-397b-a17b", - name: "Qwen 3.5 397B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 4.5 }, - limit: { context: 128000, output: 32768 }, - }, - "zai-org-glm-4.7": { - id: "zai-org-glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-24", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.65, cache_read: 0.11 }, - limit: { context: 198000, output: 16384 }, - }, - "openai-gpt-54": { - id: "openai-gpt-54", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.13, output: 18.8, cache_read: 0.313 }, - limit: { context: 1000000, output: 131072 }, - }, - "zai-org-glm-4.7-flash": { - id: "zai-org-glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 16384 }, - }, - "nvidia-nemotron-3-nano-30b-a3b": { - id: "nvidia-nemotron-3-nano-30b-a3b", - name: "NVIDIA Nemotron 3 Nano 30B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-235b-a22b": { - id: "qwen3-vl-235b-a22b", - name: "Qwen3 VL 235B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-16", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1.5 }, - limit: { context: 256000, output: 16384 }, - }, - "openai-gpt-53-codex": { - id: "openai-gpt-53-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 400000, output: 128000 }, - }, - "venice-uncensored-1-2": { - id: "venice-uncensored-1-2", - name: "Venice Uncensored 1.2", - family: "venice", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.9 }, - limit: { context: 128000, output: 8192 }, - }, - "openai-gpt-52": { - id: "openai-gpt-52", - name: "GPT-5.2", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-12-13", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 256000, output: 65536 }, - }, - "mistral-small-3-2-24b-instruct": { - id: "mistral-small-3-2-24b-instruct", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-15", - last_updated: "2026-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09375, output: 0.25 }, - limit: { context: 256000, output: 16384 }, - }, - "minimax-m27": { - id: "minimax-m27", - name: "MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.375, output: 1.5, cache_read: 0.075 }, - limit: { context: 198000, output: 32768 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen 3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-04-29", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-5-35b-a3b": { - id: "qwen3-5-35b-a3b", - name: "Qwen 3.5 35B A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-25", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3125, output: 1.25, cache_read: 0.15625 }, - limit: { context: 256000, output: 65536 }, - }, - "mercury-2": { - id: "mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-20", - last_updated: "2026-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3125, output: 0.9375, cache_read: 0.03125 }, - limit: { context: 128000, output: 50000 }, - }, - "google-gemma-3-27b-it": { - id: "google-gemma-3-27b-it", - name: "Google Gemma 3 27B Instruct", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-04", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.2 }, - limit: { context: 198000, output: 16384 }, - }, - "olafangensan-glm-4.7-flash-heretic": { - id: "olafangensan-glm-4.7-flash-heretic", - name: "GLM 4.7 Flash Heretic", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-04", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.8 }, - limit: { context: 200000, output: 24000 }, - }, - "openai-gpt-55-pro": { - id: "openai-gpt-55-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 37.5, output: 225 }, - limit: { context: 1000000, output: 128000 }, - }, - "openai-gpt-52-codex": { - id: "openai-gpt-52-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-01-15", - last_updated: "2026-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.19, output: 17.5, cache_read: 0.219 }, - limit: { context: 256000, output: 65536 }, - }, - "venice-uncensored-role-play": { - id: "venice-uncensored-role-play", - name: "Venice Role Play Uncensored", - family: "venice", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-20", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 128000, output: 4096 }, - }, - "zai-org-glm-5": { - id: "zai-org-glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 198000, output: 32000 }, - }, - "zai-org-glm-4.6": { - id: "zai-org-glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2024-04-01", - last_updated: "2026-04-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 2.75, cache_read: 0.3 }, - limit: { context: 198000, output: 16384 }, - }, - "grok-4-3": { - id: "grok-4-3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-18", - last_updated: "2026-05-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.42, - output: 2.83, - cache_read: 0.23, - context_over_200k: { input: 2.83, output: 5.67, cache_read: 0.45 }, - }, - limit: { context: 1000000, output: 32000 }, - }, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-16", - last_updated: "2026-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1875, output: 0.75 }, - limit: { context: 256000, output: 65536 }, - }, - "openai-gpt-oss-120b": { - id: "openai-gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-06", - last_updated: "2026-05-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-06", - last_updated: "2026-04-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30, cache_read: 0.6, cache_write: 7.5 }, - limit: { context: 198000, output: 32768 }, - }, - "qwen3-5-9b": { - id: "qwen3-5-9b", - name: "Qwen 3.5 9B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-04-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.15 }, - limit: { context: 256000, output: 32768 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.35, cache_read: 0.028 }, - limit: { context: 1000000, output: 32768 }, - }, - "openai-gpt-54-pro": { - id: "openai-gpt-54-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 37.5, output: 225, context_over_200k: { input: 75, output: 337.5 } }, - limit: { context: 1000000, output: 128000 }, - }, - "openai-gpt-54-mini": { - id: "openai-gpt-54-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9375, output: 5.625, cache_read: 0.09375 }, - limit: { context: 400000, output: 128000 }, - }, - "minimax-m25": { - id: "minimax-m25", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.34, output: 1.19, cache_read: 0.04 }, - limit: { context: 198000, output: 32768 }, - }, - "zai-org-glm-5-1": { - id: "zai-org-glm-5-1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.75, output: 5.5, cache_read: 0.325 }, - limit: { context: 200000, output: 24000 }, - }, - "openai-gpt-55": { - id: "openai-gpt-55", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-23", - last_updated: "2026-04-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 6.25, - output: 37.5, - cache_read: 0.625, - context_over_200k: { input: 12.5, output: 56.25, cache_read: 1.25 }, - }, - limit: { context: 1000000, output: 131072 }, - }, - "qwen3-6-27b": { - id: "qwen3-6-27b", - name: "Qwen 3.6 27B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-29", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.325, output: 3.25 }, - limit: { context: 256000, output: 65536 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 30, cache_read: 0.6, cache_write: 7.5 }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.73, output: 3.796, cache_read: 0.33 }, - limit: { context: 1000000, output: 32768 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-10", - release_date: "2025-12-04", - last_updated: "2026-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.33, output: 0.48, cache_read: 0.16 }, - limit: { context: 160000, output: 32768 }, - }, - "qwen-3-6-plus": { - id: "qwen-3-6-plus", - name: "Qwen 3.6 Plus Uncensored", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-06", - last_updated: "2026-04-12", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.625, - output: 3.75, - cache_read: 0.0625, - cache_write: 0.78, - context_over_200k: { input: 2.5, output: 7.5, cache_read: 0.0625, cache_write: 0.78 }, - }, - limit: { context: 1000000, output: 65536 }, - }, - "aion-labs-aion-2-0": { - id: "aion-labs-aion-2-0", - name: "Aion 2.0", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-24", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2, cache_read: 0.25 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-15", - last_updated: "2026-04-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.75, output: 18.75, cache_read: 0.375, cache_write: 4.69 }, - limit: { context: 198000, output: 64000 }, - }, - "openai-gpt-4o-2024-11-20": { - id: "openai-gpt-4o-2024-11-20", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-28", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3.125, output: 12.5 }, - limit: { context: 128000, output: 16384 }, - }, - "llama-3.3-70b": { - id: "llama-3.3-70b", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-06", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 128000, output: 4096 }, - }, - "kimi-k2-5": { - id: "kimi-k2-5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-01-27", - last_updated: "2026-04-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 3.5, cache_read: 0.22 }, - limit: { context: 256000, output: 65536 }, - }, - "llama-3.2-3b": { - id: "llama-3.2-3b", - name: "Llama 3.2 3B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-10-03", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "arcee-trinity-large-thinking": { - id: "arcee-trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3125, output: 1.125, cache_read: 0.075 }, - limit: { context: 256000, output: 65536 }, - }, - "hermes-3-llama-3.1-405b": { - id: "hermes-3-llama-3.1-405b", - name: "Hermes 3 Llama 3.1 405b", - family: "hermes", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-04", - release_date: "2025-09-25", - last_updated: "2026-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-3-1-pro-preview": { - id: "gemini-3-1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-03-12", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.5, - cache_write: 0.5, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1000000, output: 32768 }, - }, - "kimi-k2-6": { - id: "kimi-k2-6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 4.655, cache_read: 0.22 }, - limit: { context: 256000, output: 65536 }, - }, - "claude-opus-4-6-fast": { - id: "claude-opus-4-6-fast", - name: "Claude Opus 4.6 Fast", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 36, output: 180, cache_read: 3.6, cache_write: 45 }, - limit: { context: 1000000, output: 128000 }, - }, - "z-ai-glm-5-turbo": { - id: "z-ai-glm-5-turbo", - name: "GLM 5 Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 200000, output: 32768 }, - }, - "google-gemma-4-31b-it": { - id: "google-gemma-4-31b-it", - name: "Google Gemma 4 31B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-03", - last_updated: "2026-04-12", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.175, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - }, - }, - aihubmix: { - id: "aihubmix", - env: ["AIHUBMIX_API_KEY"], - npm: "@aihubmix/ai-sdk-provider", - name: "AIHubMix", - doc: "https://docs.aihubmix.com", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2958, output: 1.1832, cache_read: 0.05916 }, - limit: { context: 200000, output: 128000 }, - }, - "coding-glm-5.1-free": { - id: "coding-glm-5.1-free", - name: "Coding GLM 5.1 (free)", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-11", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 204800, output: 128000 }, - }, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-k2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.105 }, - limit: { context: 256000, output: 0 }, - }, - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM 5 Vision Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-05-09", - last_updated: "2026-05-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.7042, output: 3.09848, cache_read: 0.169008 }, - limit: { context: 200000, output: 128000 }, - }, - "grok-4.3": { - id: "grok-4.3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-05-01", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 2.5, - cache_read: 0.2, - context_over_200k: { input: 2.5, output: 5, cache_read: 0.4 }, - }, - limit: { context: 1000000, output: 1000000 }, - }, - "coding-minimax-m2.7-highspeed": { - id: "coding-minimax-m2.7-highspeed", - name: "Coding MiniMax M2.7 Highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 204800, output: 13100 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "coding-glm-5.1": { - id: "coding-glm-5.1", - name: "Coding-GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-11", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.22 }, - limit: { context: 200000, output: 128000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 1050000, output: 128000 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek-v4-flash-think": { - id: "deepseek-v4-flash-think", - name: "DeepSeek V4 Flash Think", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.154, output: 0.308, cache_read: 0.0308 }, - limit: { context: 1000000, output: 384000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-05-09", - last_updated: "2026-05-09", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.282, output: 1.692, cache_read: 0.0282, cache_write: 0.3525 }, - limit: { context: 991000, output: 64000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4-Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: false, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.845, output: 3.38, cache_read: 0.183112 }, - limit: { context: 200000, output: 128000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.275 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.499, cache_read: 0.03 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.25 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-15", - last_updated: "2025-11-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-6-think": { - id: "claude-opus-4-6-think", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 32000 }, - }, - "coding-minimax-m2.7-free": { - id: "coding-minimax-m2.7-free", - name: "Coding-MiniMax-M2.7-Free", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 13100 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.154, output: 0.308, cache_read: 0.0308 }, - limit: { context: 1000000, output: 384000 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.9995, cache_read: 0.160835 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.478, output: 0.956, cache_read: 0.004302 }, - limit: { context: 1000000, output: 384000 }, - }, - "claude-opus-4-7-think": { - id: "claude-opus-4-7-think", - name: "Claude Opus 4.7 Thinking", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 32000 }, - }, - "coding-minimax-m2.7": { - id: "coding-minimax-m2.7", - name: "Coding MiniMax M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 204800, output: 13100 }, - }, - "qwen3.6-max-preview": { - id: "qwen3.6-max-preview", - name: "Qwen3.6 Max Preview", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-05-09", - last_updated: "2026-05-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.268, output: 7.608, cache_read: 0.1268, cache_write: 1.585 }, - limit: { context: 240000, output: 64000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-sonnet-4-6-think": { - id: "claude-sonnet-4-6-think", - name: "Claude Sonnet 4.6 Think", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3.6-flash": { - id: "qwen3.6-flash", - name: "Qwen3.6 Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.169, output: 1.014, cache_read: 0.0169, cache_write: 0.21125 }, - limit: { context: 991000, output: 64000 }, - }, - }, - }, - cerebras: { - id: "cerebras", - env: ["CEREBRAS_API_KEY"], - npm: "@ai-sdk/cerebras", - name: "Cerebras", - doc: "https://inference-docs.cerebras.ai/models/overview", - models: { - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 32000, output: 8000 }, - }, - "qwen-3-235b-a22b-instruct-2507": { - id: "qwen-3-235b-a22b-instruct-2507", - name: "Qwen 3 235B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-22", - last_updated: "2025-07-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.2 }, - limit: { context: 131000, output: 32000 }, - }, - "zai-glm-4.7": { - id: "zai-glm-4.7", - name: "Z.AI GLM-4.7", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-10", - last_updated: "2026-01-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.25, output: 2.75, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 40000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.69 }, - limit: { context: 131072, output: 32768 }, - }, - }, - }, - lmstudio: { - id: "lmstudio", - env: ["LMSTUDIO_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "http://127.0.0.1:1234/v1", - name: "LMStudio", - doc: "https://lmstudio.ai/models", - models: { - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 32768 }, - }, - "qwen/qwen3-coder-30b": { - id: "qwen/qwen3-coder-30b", - name: "Qwen3 Coder 30B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwen3-30b-a3b-2507": { - id: "qwen/qwen3-30b-a3b-2507", - name: "Qwen3 30B A3B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - }, - }, - lucidquery: { - id: "lucidquery", - env: ["LUCIDQUERY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://lucidquery.com/api/v1", - name: "LucidQuery AI", - doc: "https://lucidquery.com/api/docs", - models: { - "lucidnova-rf1-100b": { - id: "lucidnova-rf1-100b", - name: "LucidNova RF1 100B", - family: "nova", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-09-16", - release_date: "2024-12-28", - last_updated: "2025-09-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 5 }, - limit: { context: 120000, output: 8000 }, - }, - "lucidquery-nexus-coder": { - id: "lucidquery-nexus-coder", - name: "LucidQuery Nexus Coder", - family: "lucid", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-01", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 5 }, - limit: { context: 250000, output: 60000 }, - }, - }, - }, - "moonshotai-cn": { - id: "moonshotai-cn", - env: ["MOONSHOT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.moonshot.cn/v1", - name: "Moonshot AI (China)", - doc: "https://platform.moonshot.cn/docs/api/chat", - models: { - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 131072, output: 16384 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.4, output: 10, cache_read: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-k2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "azure-cognitive-services": { - id: "azure-cognitive-services", - env: ["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME", "AZURE_COGNITIVE_SERVICES_API_KEY"], - npm: "@ai-sdk/azure", - name: "Azure Cognitive Services", - doc: "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models", - models: { - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "mai-ds-r1": { - id: "mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "codestral-2501": { - id: "codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "gpt-3.5-turbo-instruct": { - id: "gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-09-21", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "phi-4-reasoning-plus": { - id: "phi-4-reasoning-plus", - name: "Phi-4-reasoning-plus", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "cohere-embed-v3-english": { - id: "cohere-embed-v3-english", - name: "Embed v3 English", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "gpt-4-32k": { - id: "gpt-4-32k", - name: "GPT-4 32K", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 32768, output: 32768 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 272000, output: 128000 }, - }, - "phi-4": { - id: "phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-3.5-turbo-0613": { - id: "gpt-3.5-turbo-0613", - name: "GPT-3.5 Turbo 0613", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16384, output: 16384 }, - }, - "phi-3-medium-128k-instruct": { - id: "phi-3-medium-128k-instruct", - name: "Phi-3-medium-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "phi-3-small-128k-instruct": { - id: "phi-3-small-128k-instruct", - name: "Phi-3-small-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-3.5-turbo-0301": { - id: "gpt-3.5-turbo-0301", - name: "GPT-3.5 Turbo 0301", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "phi-4-mini": { - id: "phi-4-mini", - name: "Phi-4-mini", - family: "phi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "meta-llama-3-8b-instruct": { - id: "meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 8192, output: 8192 }, - }, - "phi-4-mini-reasoning": { - id: "phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "meta-llama-3.1-70b-instruct": { - id: "meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 128000, output: 32768 }, - }, - "phi-3-mini-4k-instruct": { - id: "phi-3-mini-4k-instruct", - name: "Phi-3-mini-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 4096, output: 1024 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 131072, output: 131072 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "gpt-3.5-turbo-1106": { - id: "gpt-3.5-turbo-1106", - name: "GPT-3.5 Turbo 1106", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 16384, output: 16384 }, - }, - "model-router": { - id: "model-router", - name: "Model Router", - family: "model-router", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-05-19", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-small-2503": { - id: "mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 32768 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 272000, output: 128000 }, - }, - "cohere-embed-v3-multilingual": { - id: "cohere-embed-v3-multilingual", - name: "Embed v3 Multilingual", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 66, cache_read: 8.25 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-3.5-turbo-0125": { - id: "gpt-3.5-turbo-0125", - name: "GPT-3.5 Turbo 0125", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16384, output: 16384 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "cohere-embed-v-4-0": { - id: "cohere-embed-v-4-0", - name: "Embed v4", - family: "cohere-embed", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0 }, - limit: { context: 128000, output: 1536 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-4-turbo-vision": { - id: "gpt-4-turbo-vision", - name: "GPT-4 Turbo Vision", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.1-chat": { - id: "gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "meta-llama-3.1-405b-instruct": { - id: "meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 5.33, output: 16 }, - limit: { context: 128000, output: 32768 }, - }, - "llama-3.2-11b-vision-instruct": { - id: "llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.37, output: 0.37 }, - limit: { context: 128000, output: 8192 }, - }, - "cohere-command-a": { - id: "cohere-command-a", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "deepseek-v3.2-speciale": { - id: "deepseek-v3.2-speciale", - name: "DeepSeek-V3.2-Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "llama-3.2-90b-vision-instruct": { - id: "llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.04, output: 2.04 }, - limit: { context: 128000, output: 8192 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "phi-3-small-8k-instruct": { - id: "phi-3-small-8k-instruct", - name: "Phi-3-small-instruct (8k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 8192, output: 2048 }, - }, - "meta-llama-3-70b-instruct": { - id: "meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - }, - "phi-4-reasoning": { - id: "phi-4-reasoning", - name: "Phi-4-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "phi-3-mini-128k-instruct": { - id: "phi-3-mini-128k-instruct", - name: "Phi-3-mini-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "phi-3.5-moe-instruct": { - id: "phi-3.5-moe-instruct", - name: "Phi-3.5-MoE-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.64 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5-chat": { - id: "gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-10-24", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, output: 16384 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.14, output: 4.56 }, - limit: { context: 131072, output: 131072 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "meta-llama-3.1-8b-instruct": { - id: "meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 128000, output: 32768 }, - }, - "ministral-3b": { - id: "ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 8192 }, - }, - "phi-3-medium-4k-instruct": { - id: "phi-3-medium-4k-instruct", - name: "Phi-3-medium-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 4096, output: 1024 }, - }, - "llama-4-scout-17b-16e-instruct": { - id: "llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.78 }, - limit: { context: 128000, output: 8192 }, - }, - "phi-3.5-mini-instruct": { - id: "phi-3.5-mini-instruct", - name: "Phi-3.5-mini-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-4-multimodal": { - id: "phi-4-multimodal", - name: "Phi-4-multimodal", - family: "phi", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.32, input_audio: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "codex-mini": { - id: "codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-04", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.2-chat": { - id: "gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "cohere-command-r-plus-08-2024": { - id: "cohere-command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "cohere-command-r-08-2024": { - id: "cohere-command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - }, - }, - "abliteration-ai": { - id: "abliteration-ai", - env: ["ABLIT_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.abliteration.ai/v1", - name: "abliteration.ai", - doc: "https://docs.abliteration.ai/models", - models: { - "abliterated-model": { - id: "abliterated-model", - name: "Abliterated Model", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-01-06", - last_updated: "2026-01-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 150000, input: 150000, output: 8192 }, - }, - }, - }, - "wafer.ai": { - id: "wafer.ai", - env: ["WAFER_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://pass.wafer.ai/v1", - name: "Wafer", - doc: "https://docs.wafer.ai/wafer-pass", - models: { - "Qwen3.5-397B-A17B": { - id: "Qwen3.5-397B-A17B", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "GLM-5.1": { - id: "GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "DeepSeek-V4-Pro": { - id: "DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 384000 }, - }, - }, - }, - cohere: { - id: "cohere", - env: ["COHERE_API_KEY"], - npm: "@ai-sdk/cohere", - name: "Cohere", - doc: "https://docs.cohere.com/docs/models", - models: { - "command-a-reasoning-08-2025": { - id: "command-a-reasoning-08-2025", - name: "Command A Reasoning", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 32000 }, - }, - "command-r7b-12-2024": { - id: "command-r7b-12-2024", - name: "Command R7B", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-02-27", - last_updated: "2024-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-vision-8b": { - id: "c4ai-aya-vision-8b", - name: "Aya Vision 8B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-04", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 16000, output: 4000 }, - }, - "command-r-plus-08-2024": { - id: "command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-expanse-8b": { - id: "c4ai-aya-expanse-8b", - name: "Aya Expanse 8B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-24", - last_updated: "2024-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 8000, output: 4000 }, - }, - "command-r7b-arabic-02-2025": { - id: "command-r7b-arabic-02-2025", - name: "Command R7B Arabic", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-02-27", - last_updated: "2025-02-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.0375, output: 0.15 }, - limit: { context: 128000, output: 4000 }, - }, - "command-a-vision-07-2025": { - id: "command-a-vision-07-2025", - name: "Command A Vision", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 8000 }, - }, - "c4ai-aya-vision-32b": { - id: "c4ai-aya-vision-32b", - name: "Aya Vision 32B", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-03-04", - last_updated: "2025-05-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 16000, output: 4000 }, - }, - "command-a-translate-08-2025": { - id: "command-a-translate-08-2025", - name: "Command A Translate", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 8000, output: 8000 }, - }, - "command-r-08-2024": { - id: "command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "c4ai-aya-expanse-32b": { - id: "c4ai-aya-expanse-32b", - name: "Aya Expanse 32B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-10-24", - last_updated: "2024-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 128000, output: 4000 }, - }, - "command-a-03-2025": { - id: "command-a-03-2025", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - }, - }, - "cloudferro-sherlock": { - id: "cloudferro-sherlock", - env: ["CLOUDFERRO_SHERLOCK_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api-sherlock.cloudferro.com/openai/v1/", - name: "CloudFerro Sherlock", - doc: "https://docs.sherlock.cloudferro.com/", - models: { - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10-09", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.92, output: 2.92 }, - limit: { context: 70000, output: 70000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "OpenAI GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.92, output: 2.92 }, - limit: { context: 131000, output: 131000 }, - }, - "speakleash/Bielik-11B-v3.0-Instruct": { - id: "speakleash/Bielik-11B-v3.0-Instruct", - name: "Bielik 11B v3.0 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 0.67 }, - limit: { context: 32000, output: 32000 }, - }, - "speakleash/Bielik-11B-v2.6-Instruct": { - id: "speakleash/Bielik-11B-v2.6-Instruct", - name: "Bielik 11B v2.6 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 0.67 }, - limit: { context: 32000, output: 32000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196000, input: 180000, output: 16000 }, - }, - }, - }, - "kuae-cloud-coding-plan": { - id: "kuae-cloud-coding-plan", - env: ["KUAE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding-plan-endpoint.kuaecloud.net/v1", - name: "KUAE Cloud Coding Plan", - doc: "https://docs.mthreads.com/kuaecloud/kuaecloud-doc-online/coding_plan/", - models: { - "GLM-4.7": { - id: "GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - xai: { - id: "xai", - env: ["XAI_API_KEY"], - npm: "@ai-sdk/xai", - name: "xAI", - doc: "https://docs.x.ai/docs/models", - models: { - "grok-2-1212": { - id: "grok-2-1212", - name: "Grok 2 (1212)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-12-12", - last_updated: "2024-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-vision-beta": { - id: "grok-vision-beta", - name: "Grok Vision Beta", - family: "grok-vision", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15, cache_read: 5 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-4.3": { - id: "grok-4.3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-05-01", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 2.5, - cache_read: 0.2, - context_over_200k: { input: 2.5, output: 5, cache_read: 0.4 }, - }, - limit: { context: 1000000, output: 30000 }, - }, - "grok-3-mini-fast": { - id: "grok-3-mini-fast", - name: "Grok 3 Mini Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-3-mini-latest": { - id: "grok-3-mini-latest", - name: "Grok 3 Mini Latest", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2-vision-latest": { - id: "grok-2-vision-latest", - name: "Grok 2 Vision Latest", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-mini-fast-latest": { - id: "grok-3-mini-fast-latest", - name: "Grok 3 Mini Fast Latest", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-latest": { - id: "grok-3-latest", - name: "Grok 3 Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2": { - id: "grok-2", - name: "Grok 2", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-fast-latest": { - id: "grok-3-fast-latest", - name: "Grok 3 Fast Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4.20-multi-agent-0309": { - id: "grok-4.20-multi-agent-0309", - name: "Grok 4.20 Multi-Agent", - family: "grok", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "grok-2-latest": { - id: "grok-2-latest", - name: "Grok 2 Latest", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-beta": { - id: "grok-beta", - name: "Grok Beta", - family: "grok-beta", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15, cache_read: 5 }, - limit: { context: 131072, output: 4096 }, - }, - "grok-2-vision": { - id: "grok-2-vision", - name: "Grok 2 Vision", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-2-vision-1212": { - id: "grok-2-vision-1212", - name: "Grok 2 Vision (1212)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - }, - }, - meganova: { - id: "meganova", - env: ["MEGANOVA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.meganova.ai/v1", - name: "Meganova", - doc: "https://docs.meganova.ai", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3.5-Plus": { - id: "Qwen/Qwen3.5-Plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, reasoning: 2.4 }, - limit: { context: 1000000, output: 65536 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 16384, output: 16384 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.56 }, - limit: { context: 202752, output: 131072 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.9 }, - limit: { context: 202752, output: 131072 }, - }, - "mistralai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "mistralai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "mistralai/Mistral-Nemo-Instruct-2407": { - id: "mistralai/Mistral-Nemo-Instruct-2407", - name: "Mistral Nemo Instruct 2407", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.04 }, - limit: { context: 131072, output: 65536 }, - }, - "XiaomiMiMo/MiMo-V2-Flash": { - id: "XiaomiMiMo/MiMo-V2-Flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262144, output: 32000 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 131072, output: 16384 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-08-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3-0324": { - id: "deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.88 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek-ai/DeepSeek-V3.2-Exp": { - id: "deepseek-ai/DeepSeek-V3.2-Exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-10", - last_updated: "2025-10-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-0528": { - id: "deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.15 }, - limit: { context: 163840, output: 64000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.26, output: 0.38 }, - limit: { context: 164000, output: 164000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.6 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2026-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.8 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.2 }, - limit: { context: 196608, output: 131072 }, - }, - }, - }, - "google-vertex-anthropic": { - id: "google-vertex-anthropic", - env: ["GOOGLE_VERTEX_PROJECT", "GOOGLE_VERTEX_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"], - npm: "@ai-sdk/google-vertex/anthropic", - name: "Vertex (Anthropic)", - doc: "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude", - models: { - "claude-haiku-4-5@20251001": { - id: "claude-haiku-4-5@20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-6@default": { - id: "claude-sonnet-4-6@default", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-haiku@20241022": { - id: "claude-3-5-haiku@20241022", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-3-5-sonnet@20241022": { - id: "claude-3-5-sonnet@20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-1@20250805": { - id: "claude-opus-4-1@20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-sonnet-4@20250514": { - id: "claude-sonnet-4@20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-7-sonnet@20250219": { - id: "claude-3-7-sonnet@20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4@20250514": { - id: "claude-opus-4@20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-opus-4-5@20251101": { - id: "claude-opus-4-5@20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-5@20250929": { - id: "claude-sonnet-4-5@20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6@default": { - id: "claude-opus-4-6@default", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "claude-opus-4-7@default": { - id: "claude-opus-4-7@default", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - }, - }, - evroc: { - id: "evroc", - env: ["EVROC_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.think.evroc.com/v1", - name: "evroc", - doc: "https://docs.evroc.com/products/think/overview.html", - models: { - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen3 VL 30B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.94 }, - limit: { context: 100000, output: 100000 }, - }, - "Qwen/Qwen3-Embedding-8B": { - id: "Qwen/Qwen3-Embedding-8B", - name: "Qwen3 Embedding 8B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.12 }, - limit: { context: 40960, output: 40960 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507-FP8": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507-FP8", - name: "Qwen3 30B 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.42 }, - limit: { context: 64000, output: 64000 }, - }, - "mistralai/devstral-small-2-24b-instruct-2512": { - id: "mistralai/devstral-small-2-24b-instruct-2512", - name: "Devstral Small 2 24B Instruct 2512", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.47 }, - limit: { context: 32768, output: 32768 }, - }, - "mistralai/Voxtral-Small-24B-2507": { - id: "mistralai/Voxtral-Small-24B-2507", - name: "Voxtral Small 24B", - family: "voxtral", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["audio", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 32000, output: 32000 }, - }, - "mistralai/Magistral-Small-2509": { - id: "mistralai/Magistral-Small-2509", - name: "Magistral Small 1.2 24B", - family: "magistral-small", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-06-01", - last_updated: "2025-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 2.36 }, - limit: { context: 131072, output: 131072 }, - }, - "microsoft/Phi-4-multimodal-instruct": { - id: "microsoft/Phi-4-multimodal-instruct", - name: "Phi-4 15B", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.47 }, - limit: { context: 32000, output: 32000 }, - }, - "KBLab/kb-whisper-large": { - id: "KBLab/kb-whisper-large", - name: "KB Whisper", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 448, output: 448 }, - }, - "nvidia/Llama-3.3-70B-Instruct-FP8": { - id: "nvidia/Llama-3.3-70B-Instruct-FP8", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.18, output: 1.18 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/whisper-large-v3": { - id: "openai/whisper-large-v3", - name: "Whisper 3 Large", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.00236, output: 0.00236, output_audio: 2.36 }, - limit: { context: 448, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.94 }, - limit: { context: 65536, output: 65536 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 1.47, output: 5.9 }, - limit: { context: 262144, output: 262144 }, - }, - "intfloat/multilingual-e5-large-instruct": { - id: "intfloat/multilingual-e5-large-instruct", - name: "E5 Multi-Lingual Large Embeddings 0.6B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-06-01", - last_updated: "2024-06-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.12 }, - limit: { context: 512, output: 512 }, - }, - }, - }, - synthetic: { - id: "synthetic", - env: ["SYNTHETIC_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.synthetic.new/openai/v1", - name: "Synthetic", - doc: "https://synthetic.new/pricing", - models: { - "hf:meta-llama/Llama-3.1-405B-Instruct": { - id: "hf:meta-llama/Llama-3.1-405B-Instruct", - name: "Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 3 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct": { - id: "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct", - name: "Llama-4-Scout-17B-16E-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 328000, output: 4096 }, - }, - "hf:meta-llama/Llama-3.3-70B-Instruct": { - id: "hf:meta-llama/Llama-3.3-70B-Instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-3.1-8B-Instruct": { - id: "hf:meta-llama/Llama-3.1-8B-Instruct", - name: "Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-3.1-70B-Instruct": { - id: "hf:meta-llama/Llama-3.1-70B-Instruct", - name: "Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - id: "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 524000, output: 4096 }, - }, - "hf:MiniMaxAI/MiniMax-M2": { - id: "hf:MiniMaxAI/MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 196608, output: 131000 }, - }, - "hf:MiniMaxAI/MiniMax-M2.5": { - id: "hf:MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-07", - last_updated: "2026-02-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.6 }, - limit: { context: 191488, output: 65536 }, - }, - "hf:MiniMaxAI/MiniMax-M2.1": { - id: "hf:MiniMaxAI/MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 204800, output: 131072 }, - }, - "hf:Qwen/Qwen3.5-397B-A17B": { - id: "hf:Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5-97B-A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.6 }, - limit: { context: 262144, output: 65536 }, - status: "beta", - }, - "hf:Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "hf:Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-11-11", - last_updated: "2024-11-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 0.8 }, - limit: { context: 32768, output: 32768 }, - }, - "hf:Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "hf:Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen 3 235B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "hf:Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 3 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen 3 Coder 480B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 256000, output: 32000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.1": { - id: "hf:deepseek-ai/DeepSeek-V3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3-0324": { - id: "hf:deepseek-ai/DeepSeek-V3-0324", - name: "DeepSeek V3 (0324)", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3": { - id: "hf:deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-R1": { - id: "hf:deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-R1-0528": { - id: "hf:deepseek-ai/DeepSeek-R1-0528", - name: "DeepSeek R1 (0528)", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 8 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.2": { - id: "hf:deepseek-ai/DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.4, cache_read: 0.27, cache_write: 0 }, - limit: { context: 162816, input: 162816, output: 8000 }, - }, - "hf:deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "hf:deepseek-ai/DeepSeek-V3.1-Terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-09-22", - last_updated: "2025-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 128000, output: 128000 }, - }, - "hf:openai/gpt-oss-120b": { - id: "hf:openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 32768 }, - }, - "hf:moonshotai/Kimi-K2-Thinking": { - id: "hf:moonshotai/Kimi-K2-Thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-07", - last_updated: "2025-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 262144 }, - }, - "hf:moonshotai/Kimi-K2-Instruct-0905": { - id: "hf:moonshotai/Kimi-K2-Instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 1.2 }, - limit: { context: 262144, output: 32768 }, - }, - "hf:moonshotai/Kimi-K2.5": { - id: "hf:moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4": { - id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1, cache_read: 0.3 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:nvidia/Kimi-K2.5-NVFP4": { - id: "hf:nvidia/Kimi-K2.5-NVFP4", - name: "Kimi K2.5 (NVFP4)", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 262144, output: 65536 }, - }, - "hf:zai-org/GLM-4.7-Flash": { - id: "hf:zai-org/GLM-4.7-Flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-18", - last_updated: "2026-01-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.06 }, - limit: { context: 196608, output: 65536 }, - }, - "hf:zai-org/GLM-4.7": { - id: "hf:zai-org/GLM-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 200000, output: 64000 }, - }, - "hf:zai-org/GLM-5.1": { - id: "hf:zai-org/GLM-5.1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-04-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 1 }, - limit: { context: 196608, output: 65536 }, - }, - "hf:zai-org/GLM-5": { - id: "hf:zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 1 }, - limit: { context: 196608, output: 65536 }, - }, - "hf:zai-org/GLM-4.6": { - id: "hf:zai-org/GLM-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.19 }, - limit: { context: 200000, output: 64000 }, - }, - "hf:moonshotai/Kimi-K2.6": { - id: "hf:moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.95 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - nvidia: { - id: "nvidia", - env: ["NVIDIA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://integrate.api.nvidia.com/v1", - name: "Nvidia", - doc: "https://docs.api.nvidia.com/nim/", - models: { - "black-forest-labs/flux.1-dev": { - id: "black-forest-labs/flux.1-dev", - name: "FLUX.1-dev", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 0 }, - }, - "stepfun-ai/step-3.5-flash": { - id: "stepfun-ai/step-3.5-flash", - name: "Step 3.5 Flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-02", - last_updated: "2026-02-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 16384 }, - }, - "mistralai/codestral-22b-instruct-v0.1": { - id: "mistralai/codestral-22b-instruct-v0.1", - name: "Codestral 22b Instruct V0.1", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-05-29", - last_updated: "2024-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mistral-large-3-675b-instruct-2512": { - id: "mistralai/mistral-large-3-675b-instruct-2512", - name: "Mistral Large 3 675B Instruct 2512", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/devstral-2-123b-instruct-2512": { - id: "mistralai/devstral-2-123b-instruct-2512", - name: "Devstral-2-123B-Instruct-2512", - family: "devstral", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-08", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/ministral-14b-instruct-2512": { - id: "mistralai/ministral-14b-instruct-2512", - name: "Ministral 3 14B Instruct 2512", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-01", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-small-3.1-24b-instruct-2503": { - id: "mistralai/mistral-small-3.1-24b-instruct-2503", - name: "Mistral Small 3.1 24b Instruct 2503", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-11", - last_updated: "2025-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mamba-codestral-7b-v0.1": { - id: "mistralai/mamba-codestral-7b-v0.1", - name: "Mamba Codestral 7b V0.1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistralai/mistral-large-2-instruct": { - id: "mistralai/mistral-large-2-instruct", - name: "Mistral Large 2 Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-24", - last_updated: "2024-07-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-4k-instruct": { - id: "microsoft/phi-3-medium-4k-instruct", - name: "Phi 3 Medium 4k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4000, output: 4096 }, - }, - "microsoft/phi-3.5-moe-instruct": { - id: "microsoft/phi-3.5-moe-instruct", - name: "Phi 3.5 Moe Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-17", - last_updated: "2024-08-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-mini-instruct": { - id: "microsoft/phi-4-mini-instruct", - name: "Phi-4-Mini", - family: "phi", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "microsoft/phi-3-small-8k-instruct": { - id: "microsoft/phi-3-small-8k-instruct", - name: "Phi 3 Small 8k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8000, output: 4096 }, - }, - "microsoft/phi-3-vision-128k-instruct": { - id: "microsoft/phi-3-vision-128k-instruct", - name: "Phi 3 Vision 128k Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-05-19", - last_updated: "2024-05-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3.5-vision-instruct": { - id: "microsoft/phi-3.5-vision-instruct", - name: "Phi 3.5 Vision Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-16", - last_updated: "2024-08-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-128k-instruct": { - id: "microsoft/phi-3-small-128k-instruct", - name: "Phi 3 Small 128k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-128k-instruct": { - id: "microsoft/phi-3-medium-128k-instruct", - name: "Phi 3 Medium 128k Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-07", - last_updated: "2024-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { - id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", - name: "Nemotron 3 Nano Omni", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-28", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 65536 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1", - name: "Llama 3.3 Nemotron Super 49b V1", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemotron-4-340b-instruct": { - id: "nvidia/nemotron-4-340b-instruct", - name: "Nemotron 4 340b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-06-13", - last_updated: "2024-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama3-chatqa-1.5-70b": { - id: "nvidia/llama3-chatqa-1.5-70b", - name: "Llama3 Chatqa 1.5 70b", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-28", - last_updated: "2024-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "Llama 3.3 Nemotron Super 49b V1.5", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-16", - last_updated: "2025-03-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/parakeet-tdt-0.6b-v2": { - id: "nvidia/parakeet-tdt-0.6b-v2", - name: "Parakeet TDT 0.6B v2", - family: "parakeet", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "nemotron-3-nano-30b-a3b", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/llama-3.1-nemotron-ultra-253b-v1": { - id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", - name: "Llama-3.1-Nemotron-Ultra-253B-v1", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nvidia-nemotron-nano-9b-v2": { - id: "nvidia/nvidia-nemotron-nano-9b-v2", - name: "nvidia-nemotron-nano-9b-v2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/cosmos-nemotron-34b": { - id: "nvidia/cosmos-nemotron-34b", - name: "Cosmos Nemotron 34B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/llama-embed-nemotron-8b": { - id: "nvidia/llama-embed-nemotron-8b", - name: "Llama Embed Nemotron 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-03", - release_date: "2025-03-18", - last_updated: "2025-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 2048 }, - }, - "nvidia/llama-3.1-nemotron-51b-instruct": { - id: "nvidia/llama-3.1-nemotron-51b-instruct", - name: "Llama 3.1 Nemotron 51b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-22", - last_updated: "2024-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/llama-3.1-nemotron-70b-instruct": { - id: "nvidia/llama-3.1-nemotron-70b-instruct", - name: "Llama 3.1 Nemotron 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-10-12", - last_updated: "2024-10-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "nvidia/nemoretriever-ocr-v1": { - id: "nvidia/nemoretriever-ocr-v1", - name: "NeMo Retriever OCR v1", - family: "nemoretriever", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-01", - last_updated: "2025-09-05", - modalities: { input: ["image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "deepseek-ai/deepseek-r1": { - id: "deepseek-ai/deepseek-r1", - name: "Deepseek R1", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.1": { - id: "deepseek-ai/deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-20", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-ai/deepseek-coder-6.7b-instruct": { - id: "deepseek-ai/deepseek-coder-6.7b-instruct", - name: "Deepseek Coder 6.7b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2023-10-29", - last_updated: "2023-10-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.2": { - id: "deepseek-ai/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 163840, output: 65536 }, - }, - "deepseek-ai/deepseek-r1-0528": { - id: "deepseek-ai/deepseek-r1-0528", - name: "Deepseek R1 0528", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-ai/deepseek-v3.1-terminus": { - id: "deepseek-ai/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/whisper-large-v3": { - id: "openai/whisper-large-v3", - name: "Whisper Large v3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - family: "gpt-oss", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-04", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "minimaxai/minimax-m2.7": { - id: "minimaxai/minimax-m2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-04-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimaxai/minimax-m2.1": { - id: "minimaxai/minimax-m2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimaxai/minimax-m2.5": { - id: "minimaxai/minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm4.7": { - id: "z-ai/glm4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "z-ai/glm5": { - id: "z-ai/glm5", - name: "GLM5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 202752, output: 131000 }, - }, - "z-ai/glm-5.1": { - id: "z-ai/glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 131072 }, - }, - "meta/llama-4-scout-17b-16e-instruct": { - id: "meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17b 16e Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-02", - release_date: "2025-04-02", - last_updated: "2025-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama 3.3 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-26", - last_updated: "2024-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama3-8b-instruct": { - id: "meta/llama3-8b-instruct", - name: "Llama3 8b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama3-70b-instruct": { - id: "meta/llama3-70b-instruct", - name: "Llama3 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/codellama-70b": { - id: "meta/codellama-70b", - name: "Codellama 70b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-01-29", - last_updated: "2024-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11b Vision Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.1-70b-instruct": { - id: "meta/llama-3.1-70b-instruct", - name: "Llama 3.1 70b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.2-1b-instruct": { - id: "meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-maverick-17b-128e-instruct": { - id: "meta/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17b 128e Instruct", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-02", - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-3.1-405b-instruct": { - id: "meta/llama-3.1-405b-instruct", - name: "Llama 3.1 405b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3-235b-a22b": { - id: "qwen/qwen3-235b-a22b", - name: "Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen/qwen2.5-coder-7b-instruct": { - id: "qwen/qwen2.5-coder-7b-instruct", - name: "Qwen2.5 Coder 7b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-17", - last_updated: "2024-09-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen2.5-coder-32b-instruct": { - id: "qwen/qwen2.5-coder-32b-instruct", - name: "Qwen2.5 Coder 32b Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-06", - last_updated: "2024-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3.5-397b-a17b": { - id: "qwen/qwen3.5-397b-a17b", - name: "Qwen3.5-397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 8192 }, - }, - "qwen/qwen3-next-80b-a3b-thinking": { - id: "qwen/qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen/qwq-32b": { - id: "qwen/qwq-32b", - name: "Qwq 32b", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen/qwen3-next-80b-a3b-instruct": { - id: "qwen/qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 66536 }, - }, - "google/gemma-2-2b-it": { - id: "google/gemma-2-2b-it", - name: "Gemma 2 2b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-16", - last_updated: "2024-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3n-e4b-it": { - id: "google/gemma-3n-e4b-it", - name: "Gemma 3n E4b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-06-03", - last_updated: "2025-06-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3-1b-it": { - id: "google/gemma-3-1b-it", - name: "Gemma 3 1b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-10", - last_updated: "2025-03-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/codegemma-7b": { - id: "google/codegemma-7b", - name: "Codegemma 7b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-03-21", - last_updated: "2024-03-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma-4-31B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 16384 }, - }, - "google/gemma-3-12b-it": { - id: "google/gemma-3-12b-it", - name: "Gemma 3 12b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/codegemma-1.1-7b": { - id: "google/codegemma-1.1-7b", - name: "Codegemma 1.1 7b", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-04-30", - last_updated: "2024-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3n-e2b-it": { - id: "google/gemma-3n-e2b-it", - name: "Gemma 3n E2b It", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-06-12", - last_updated: "2025-06-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-2-27b-it": { - id: "google/gemma-2-27b-it", - name: "Gemma 2 27b It", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-06-24", - last_updated: "2024-06-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma-3-27B-IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2024-12-01", - last_updated: "2025-09-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-07", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-01-01", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/mistral-medium-3.5-128b": { - id: "mistralai/mistral-medium-3.5-128b", - name: "Mistral Medium 3.5 128B", - family: "mistral-medium", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-29", - last_updated: "2026-04-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/deepseek-v4-flash": { - id: "deepseek-ai/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1048576, output: 393216 }, - }, - "deepseek-ai/deepseek-v4-pro": { - id: "deepseek-ai/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1048576, output: 393216 }, - }, - }, - }, - inference: { - id: "inference", - env: ["INFERENCE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://inference.net/v1", - name: "Inference", - doc: "https://inference.net/models", - models: { - "mistral/mistral-nemo-12b-instruct": { - id: "mistral/mistral-nemo-12b-instruct", - name: "Mistral Nemo 12B Instruct", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.038, output: 0.1 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.055, output: 0.055 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-1b-instruct": { - id: "meta/llama-3.2-1b-instruct", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0.01 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.2-3b-instruct": { - id: "meta/llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 16000, output: 4096 }, - }, - "meta/llama-3.1-8b-instruct": { - id: "meta/llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.025, output: 0.025 }, - limit: { context: 16000, output: 4096 }, - }, - "qwen/qwen-2.5-7b-vision-instruct": { - id: "qwen/qwen-2.5-7b-vision-instruct", - name: "Qwen 2.5 7B Vision Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 125000, output: 4096 }, - }, - "qwen/qwen3-embedding-4b": { - id: "qwen/qwen3-embedding-4b", - name: "Qwen 3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 32000, output: 2048 }, - }, - "google/gemma-3": { - id: "google/gemma-3", - name: "Google Gemma 3", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.3 }, - limit: { context: 125000, output: 4096 }, - }, - "osmosis/osmosis-structure-0.6b": { - id: "osmosis/osmosis-structure-0.6b", - name: "Osmosis Structure 0.6B", - family: "osmosis", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 4000, output: 2048 }, - }, - }, - }, - inception: { - id: "inception", - env: ["INCEPTION_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.inceptionlabs.ai/v1/", - name: "Inception", - doc: "https://platform.inceptionlabs.ai/docs", - models: { - "mercury-edit-2": { - id: "mercury-edit-2", - name: "Mercury Edit 2", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 8192 }, - }, - "mercury-2": { - id: "mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01-01", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 50000 }, - }, - }, - }, - openai: { - id: "openai", - env: ["OPENAI_API_KEY"], - npm: "@ai-sdk/openai", - name: "OpenAI", - doc: "https://platform.openai.com/docs/models", - models: { - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-05-13": { - id: "gpt-4o-2024-05-13", - name: "GPT-4o (2024-05-13)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 15 }, - limit: { context: 128000, output: 4096 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 12.5, output: 75, cache_read: 1.25 }, - provider: { body: { service_tier: "priority" } }, - }, - }, - }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2022-12", - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-06-10", - last_updated: "2025-06-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "o4-mini-deep-research": { - id: "o4-mini-deep-research", - name: "o4-mini-deep-research", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 1.5, output: 9, cache_read: 0.15 }, - provider: { body: { service_tier: "priority" } }, - }, - }, - }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1": { - id: "gpt-image-1", - name: "gpt-image-1", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-24", - last_updated: "2025-04-24", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-4o-2024-08-06": { - id: "gpt-4o-2024-08-06", - name: "GPT-4o (2024-08-06)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-08-06", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1-mini": { - id: "gpt-image-1-mini", - name: "gpt-image-1-mini", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-09-26", - last_updated: "2025-09-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-3.5-turbo": { - id: "gpt-3.5-turbo", - name: "GPT-3.5-turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2021-09-01", - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 1.25 }, - limit: { context: 16385, output: 4096 }, - }, - "o3-deep-research": { - id: "o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, output: 100000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2024-01", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "o1-pro": { - id: "o1-pro", - name: "o1-pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2025-03-19", - last_updated: "2025-03-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 150, output: 600 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - experimental: { - modes: { - fast: { cost: { input: 5, output: 30, cache_read: 0.5 }, provider: { body: { service_tier: "priority" } } }, - }, - }, - }, - "gpt-5.1-chat-latest": { - id: "gpt-5.1-chat-latest", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.3-codex-spark": { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - family: "gpt-codex-spark", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 100000, output: 32000 }, - }, - "chatgpt-image-latest": { - id: "chatgpt-image-latest", - name: "chatgpt-image-latest", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 272000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-image-1.5": { - id: "gpt-image-1.5", - name: "gpt-image-1.5", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, input: 0, output: 0 }, - }, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-4o-2024-11-20": { - id: "gpt-4o-2024-11-20", - name: "GPT-4o (2024-11-20)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - }, - }, - requesty: { - id: "requesty", - env: ["REQUESTY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://router.requesty.ai/v1", - name: "Requesty", - doc: "https://requesty.ai/solution/llm-routing/models", - models: { - "xai/grok-4-fast": { - id: "xai/grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05, cache_write: 0.2 }, - limit: { context: 2000000, output: 64000 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-09", - last_updated: "2025-09-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 3 }, - limit: { context: 256000, output: 64000 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT-5.1-Codex-Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 9, cache_read: 0.11 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 128000, output: 32000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 16000, output: 4000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT-5.3-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.1-chat": { - id: "openai/gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4 Mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1-Codex-Mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 100000 }, - }, - "openai/gpt-5-image": { - id: "openai/gpt-5-image", - name: "GPT-5 Image", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-10-14", - last_updated: "2025-10-14", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 5, output: 10, cache_read: 1.25 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.1": { - id: "openai/gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, cache_read: 30 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "audio", "image", "video"], output: ["text", "audio", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 Mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "google/gemini-3-flash-preview": { - id: "google/gemini-3-flash-preview", - name: "Gemini 3 Flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, cache_write: 4.5 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.55 }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-01", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 62000 }, - }, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3-7-sonnet": { - id: "anthropic/claude-3-7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.31, - cache_write: 2.375, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "anthropic/claude-opus-4-1": { - id: "anthropic/claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, - digitalocean: { - id: "digitalocean", - env: ["DIGITALOCEAN_ACCESS_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://inference.do-ai.run/v1", - name: "DigitalOcean", - doc: "https://docs.digitalocean.com/products/gradient-ai-platform/details/models/", - models: { - "openai-gpt-4o-mini": { - id: "openai-gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.075 }, - limit: { context: 128000, output: 16384 }, - }, - "multi-qa-mpnet-base-dot-v1": { - id: "multi-qa-mpnet-base-dot-v1", - name: "Multi-QA-mpnet-base-dot-v1", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2021-08-30", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.009, output: 0 }, - limit: { context: 512, output: 768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.7 }, - limit: { context: 262144, output: 32768 }, - }, - "nemotron-3-nano-omni": { - id: "nemotron-3-nano-omni", - name: "Nemotron Nano 3 Omni", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-28", - last_updated: "2026-04-30", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 0.9 }, - limit: { context: 65536, output: 65536 }, - }, - "llama3-8b-instruct": { - id: "llama3-8b-instruct", - name: "Llama 3.1 Instruct (8B)", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.198, output: 0.198 }, - limit: { context: 131072, output: 131072 }, - }, - "anthropic-claude-opus-4.7": { - id: "anthropic-claude-opus-4.7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic-claude-sonnet-4": { - id: "anthropic-claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.3, cache_write: 3.75 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "wan2-2-t2v-a14b": { - id: "wan2-2-t2v-a14b", - name: "Wan2.2-T2V-A14B", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-07-28", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["video"] }, - open_weights: true, - cost: { input: 0.6, output: 0 }, - limit: { context: 100, output: 1 }, - }, - "qwen-2.5-14b-instruct": { - id: "qwen-2.5-14b-instruct", - name: "Qwen 2.5 14B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 131072, output: 131072 }, - }, - "openai-gpt-5.4": { - id: "openai-gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen 3.5 397B A17B", - family: "qwen3.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 3.5 }, - limit: { context: 262144, output: 81920 }, - }, - "openai-o3": { - id: "openai-o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "e5-large-v2": { - id: "e5-large-v2", - name: "E5 Large v2", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-05-19", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "openai-gpt-5.2-pro": { - id: "openai-gpt-5.2-pro", - name: "GPT-5.2 pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, output: 128000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - release_date: "2026-02-11", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 202752, output: 128000 }, - }, - "openai-gpt-5.4-nano": { - id: "openai-gpt-5.4-nano", - name: "GPT-5.4 nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, output: 128000 }, - }, - "mistral-7b-instruct-v0.3": { - id: "mistral-7b-instruct-v0.3", - name: "Mistral 7B Instruct v0.3", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-05-22", - last_updated: "2024-05-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 32768, output: 32768 }, - }, - "llama3.3-70b-instruct": { - id: "llama3.3-70b-instruct", - name: "Llama 3.3 Instruct 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.65, output: 0.65 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-3-14B": { - id: "mistral-3-14B", - name: "Ministral 3 14B Instruct", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-15", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, output: 128000 }, - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-30", - last_updated: "2025-01-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.99, output: 0.99 }, - limit: { context: 131072, output: 32768 }, - }, - "alibaba-qwen3-32b": { - id: "alibaba-qwen3-32b", - name: "Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.55 }, - limit: { context: 131000, output: 40960 }, - }, - "anthropic-claude-opus-4.5": { - id: "anthropic-claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "openai-o1": { - id: "openai-o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "anthropic-claude-3-opus": { - id: "anthropic-claude-3-opus", - name: "Claude 3 Opus", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - status: "deprecated", - }, - "stable-diffusion-3.5-large": { - id: "stable-diffusion-3.5-large", - name: "Stable Diffusion 3.5 Large", - family: "stable-diffusion", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10-22", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["image"] }, - open_weights: true, - cost: { input: 0.08, output: 0 }, - limit: { context: 256, output: 1 }, - }, - "openai-gpt-5-nano": { - id: "openai-gpt-5-nano", - name: "GPT-5 nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "llama-4-maverick": { - id: "llama-4-maverick", - name: "Llama 4 Maverick 17B 128E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2026-04-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.87 }, - limit: { context: 1000000, output: 16384 }, - }, - "anthropic-claude-4.5-sonnet": { - id: "anthropic-claude-4.5-sonnet", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.3, cache_write: 3.75 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "qwen3-embedding-0.6b": { - id: "qwen3-embedding-0.6b", - name: "Qwen3 Embedding 0.6B", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06-03", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0 }, - limit: { context: 8000, output: 1024 }, - status: "beta", - }, - "anthropic-claude-4.5-haiku": { - id: "anthropic-claude-4.5-haiku", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "gte-large-en-v1.5": { - id: "gte-large-en-v1.5", - name: "GTE Large (v1.5)", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03-27", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0 }, - limit: { context: 8192, output: 1024 }, - }, - "openai-gpt-4.1": { - id: "openai-gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "anthropic-claude-3.5-haiku": { - id: "anthropic-claude-3.5-haiku", - name: "Claude 3.5 Haiku", - family: "claude-haiku", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-11-05", - last_updated: "2024-11-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - }, - "openai-gpt-5.2": { - id: "openai-gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "deepseek-3.2": { - id: "deepseek-3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-12-02", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.6 }, - limit: { context: 128000, output: 64000 }, - }, - "nemotron-3-nano-30b": { - id: "nemotron-3-nano-30b", - name: "Nemotron 3 Nano 30B A3B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "anthropic-claude-opus-4": { - id: "anthropic-claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "openai-gpt-oss-20b": { - id: "openai-gpt-oss-20b", - name: "gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-05", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.45 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.7 }, - limit: { context: 262144, output: 65536 }, - }, - "openai-o3-mini": { - id: "openai-o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai-gpt-oss-120b": { - id: "openai-gpt-oss-120b", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-05", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.7 }, - limit: { context: 131072, output: 131072 }, - }, - "gemma-4-31B-it": { - id: "gemma-4-31B-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-04-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.5 }, - limit: { context: 256000, output: 8192 }, - }, - "nemotron-nano-12b-v2-vl": { - id: "nemotron-nano-12b-v2-vl", - name: "Nemotron Nano 12B v2 VL", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-01", - last_updated: "2026-04-30", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "anthropic-claude-4.1-opus": { - id: "anthropic-claude-4.1-opus", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4 }, - limit: { context: 262144, output: 262144 }, - }, - "openai-gpt-image-2": { - id: "openai-gpt-image-2", - name: "GPT Image 2", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-24", - last_updated: "2025-04-24", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "anthropic-claude-4.6-sonnet": { - id: "anthropic-claude-4.6-sonnet", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.3, cache_write: 3.75 }, - }, - limit: { context: 1000000, output: 64000 }, - }, - "openai-gpt-5-mini": { - id: "openai-gpt-5-mini", - name: "GPT-5 mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "anthropic-claude-haiku-4.5": { - id: "anthropic-claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48 }, - limit: { context: 1048576, output: 393216 }, - }, - "ministral-3-8b-instruct-2512": { - id: "ministral-3-8b-instruct-2512", - name: "Ministral 3 8B", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 262144, output: 262144 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2026-02-12", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 128000 }, - status: "beta", - }, - "openai-gpt-image-1": { - id: "openai-gpt-image-1", - name: "GPT Image 1", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-24", - last_updated: "2025-04-24", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 5, output: 40, cache_read: 1.25 }, - limit: { context: 0, output: 0 }, - }, - "openai-gpt-5.5": { - id: "openai-gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1000000, output: 128000 }, - }, - "nvidia-nemotron-3-super-120b": { - id: "nvidia-nemotron-3-super-120b", - name: "Nemotron-3-Super-120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-02", - release_date: "2026-03-11", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.65 }, - limit: { context: 256000, output: 32768 }, - status: "beta", - }, - "openai-gpt-5.4-pro": { - id: "openai-gpt-5.4-pro", - name: "GPT-5.4 pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 400000, output: 128000 }, - }, - "all-mini-lm-l6-v2": { - id: "all-mini-lm-l6-v2", - name: "All-MiniLM-L6-v2", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2021-08-30", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.009, output: 0 }, - limit: { context: 256, output: 384 }, - }, - "bge-m3": { - id: "bge-m3", - name: "BGE M3", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01-30", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, output: 1024 }, - }, - "openai-gpt-5.1-codex-max": { - id: "openai-gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "anthropic-claude-opus-4.6": { - id: "anthropic-claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 0.5, cache_write: 6.25 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "openai-gpt-4o": { - id: "openai-gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai-gpt-5.4-mini": { - id: "openai-gpt-5.4-mini", - name: "GPT-5.4 mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, output: 128000 }, - }, - "openai-gpt-5": { - id: "openai-gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "arcee-trinity-large-thinking": { - id: "arcee-trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.9, cache_read: 0.06 }, - limit: { context: 256000, output: 128000 }, - status: "beta", - }, - "mistral-nemo-instruct-2407": { - id: "mistral-nemo-instruct-2407", - name: "Mistral Nemo Instruct", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - status: "deprecated", - }, - "deepseek-v3": { - id: "deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - limit: { context: 163840, output: 131072 }, - }, - "bge-reranker-v2-m3": { - id: "bge-reranker-v2-m3", - name: "BGE Reranker v2 M3", - family: "bge", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03-12", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.01, output: 0 }, - limit: { context: 8192, output: 1 }, - }, - "anthropic-claude-3.7-sonnet": { - id: "anthropic-claude-3.7-sonnet", - name: "Claude 3.7 Sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - status: "deprecated", - }, - "qwen3-tts-voicedesign": { - id: "qwen3-tts-voicedesign", - name: "Qwen3 TTS VoiceDesign", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2026-04-21", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - limit: { context: 32768, output: 1 }, - }, - "openai-gpt-image-1.5": { - id: "openai-gpt-image-1.5", - name: "GPT Image 1.5", - family: "gpt-image", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-11-25", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["image"] }, - open_weights: false, - cost: { input: 5, output: 10, cache_read: 1 }, - limit: { context: 0, output: 0 }, - }, - "openai-gpt-5.3-codex": { - id: "openai-gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "anthropic-claude-3.5-sonnet": { - id: "anthropic-claude-3.5-sonnet", - name: "Claude 3.5 Sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-06-20", - last_updated: "2024-10-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - }, - "fal-ai/fast-sdxl": { - id: "fal-ai/fast-sdxl", - name: "Fast SDXL", - family: "stable-diffusion", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-07-26", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["image"] }, - open_weights: true, - limit: { context: 0, output: 0 }, - }, - "fal-ai/flux/schnell": { - id: "fal-ai/flux/schnell", - name: "FLUX.1 [schnell]", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-01", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["image"] }, - open_weights: true, - limit: { context: 0, output: 0 }, - }, - "fal-ai/elevenlabs/tts/multilingual-v2": { - id: "fal-ai/elevenlabs/tts/multilingual-v2", - name: "ElevenLabs Multilingual TTS v2", - family: "elevenlabs", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-08-22", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "fal-ai/stable-audio-25/text-to-audio": { - id: "fal-ai/stable-audio-25/text-to-audio", - name: "Stable Audio 2.5 (Text-to-Audio)", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-10-08", - last_updated: "2026-04-16", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - }, - }, - vultr: { - id: "vultr", - env: ["VULTR_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.vultrinference.com/v1", - name: "Vultr", - doc: "https://api.vultrinference.com/", - models: { - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-02-11", - last_updated: "2025-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 194000, output: 4096 }, - }, - "GLM-5-FP8": { - id: "GLM-5-FP8", - name: "GLM 5 FP8", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.85, output: 3.1 }, - limit: { context: 200000, output: 131072 }, - }, - "DeepSeek-V3.2": { - id: "DeepSeek-V3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 1.65 }, - limit: { context: 127000, output: 4096 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 129000, output: 4096 }, - }, - "Kimi-K2.5": { - id: "Kimi-K2.5", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.75 }, - limit: { context: 254000, output: 32768 }, - }, - }, - }, - "alibaba-coding-plan-cn": { - id: "alibaba-coding-plan-cn", - env: ["ALIBABA_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://coding.dashscope.aliyuncs.com/v1", - name: "Alibaba Coding Plan (China)", - doc: "https://help.aliyun.com/zh/model-studio/coding-plan", - models: { - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 196608, output: 24576 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.5-plus": { - id: "qwen3.5-plus", - name: "Qwen3.5 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 65536 }, - }, - }, - }, - mistral: { - id: "mistral", - env: ["MISTRAL_API_KEY"], - npm: "@ai-sdk/mistral", - name: "Mistral", - doc: "https://docs.mistral.ai/getting-started/models/", - models: { - "mistral-small-latest": { - id: "mistral-small-latest", - name: "Mistral Small (latest)", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "labs-devstral-small-2512": { - id: "labs-devstral-small-2512", - name: "Devstral Small 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 256000 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "magistral-medium-latest": { - id: "magistral-medium-latest", - name: "Magistral Medium (latest)", - family: "magistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 5 }, - limit: { context: 128000, output: 16384 }, - }, - "open-mixtral-8x7b": { - id: "open-mixtral-8x7b", - name: "Mixtral 8x7B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 0.7 }, - limit: { context: 32000, output: 32000 }, - }, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 2.1", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 131072, output: 16384 }, - }, - "codestral-latest": { - id: "codestral-latest", - name: "Codestral (latest)", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-05-29", - last_updated: "2025-01-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 4096 }, - }, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "pixtral-12b": { - id: "pixtral-12b", - name: "Pixtral 12B", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "ministral-8b-latest": { - id: "ministral-8b-latest", - name: "Ministral 8B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-embed": { - id: "mistral-embed", - name: "Mistral Embed", - family: "mistral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8000, output: 3072 }, - }, - "magistral-small": { - id: "magistral-small", - name: "Magistral Small", - family: "magistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-small-2603": { - id: "mistral-small-2603", - name: "Mistral Small 4", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "ministral-3b-latest": { - id: "ministral-3b-latest", - name: "Ministral 3B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 128000 }, - }, - "open-mixtral-8x22b": { - id: "open-mixtral-8x22b", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 64000, output: 64000 }, - }, - "mistral-medium-2604": { - id: "mistral-medium-2604", - name: "Mistral Medium 3.5", - family: "mistral-medium", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-29", - last_updated: "2026-04-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 262144 }, - }, - "devstral-small-2505": { - id: "devstral-small-2505", - name: "Devstral Small 2505", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 128000 }, - }, - "devstral-medium-2507": { - id: "devstral-medium-2507", - name: "Devstral Medium", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "open-mistral-7b": { - id: "open-mistral-7b", - name: "Mistral 7B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2023-09-27", - last_updated: "2023-09-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.25 }, - limit: { context: 8000, output: 8000 }, - }, - "devstral-medium-latest": { - id: "devstral-medium-latest", - name: "Devstral 2 (latest)", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131072, output: 131072 }, - }, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral-medium-2508": { - id: "mistral-medium-2508", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-08-12", - last_updated: "2025-08-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-medium-latest": { - id: "mistral-medium-latest", - name: "Mistral Medium (latest)", - family: "mistral-medium", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-29", - last_updated: "2026-04-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - ovhcloud: { - id: "ovhcloud", - env: ["OVHCLOUD_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", - name: "OVHcloud AI Endpoints", - doc: "https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog//", - models: { - "meta-llama-3_3-70b-instruct": { - id: "meta-llama-3_3-70b-instruct", - name: "Meta-Llama-3_3-70B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.74, output: 0.74 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-7b-instruct-v0.3": { - id: "mistral-7b-instruct-v0.3", - name: "Mistral-7B-Instruct-v0.3", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-01", - last_updated: "2025-04-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.11 }, - limit: { context: 65536, output: 65536 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3-32B", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-16", - last_updated: "2025-07-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.25 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen2.5-vl-72b-instruct": { - id: "qwen2.5-vl-72b-instruct", - name: "Qwen2.5-VL-72B-Instruct", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-03-31", - last_updated: "2025-03-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.01, output: 1.01 }, - limit: { context: 32768, output: 32768 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder-30B-A3B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-28", - last_updated: "2025-10-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.26 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "gpt-oss-20b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.18 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-small-3.2-24b-instruct-2506": { - id: "mistral-small-3.2-24b-instruct-2506", - name: "Mistral-Small-3.2-24B-Instruct-2506", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-16", - last_updated: "2025-07-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.31 }, - limit: { context: 131072, output: 131072 }, - }, - "qwen3.5-9b": { - id: "qwen3.5-9b", - name: "Qwen3.5-9B", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.47 }, - limit: { context: 131072, output: 131072 }, - }, - "mistral-nemo-instruct-2407": { - id: "mistral-nemo-instruct-2407", - name: "Mistral-Nemo-Instruct-2407", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-20", - last_updated: "2024-11-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 65536, output: 65536 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama-3.1-8B-Instruct", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-11", - last_updated: "2025-06-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.11 }, - limit: { context: 131072, output: 131072 }, - }, - }, - }, - friendli: { - id: "friendli", - env: ["FRIENDLI_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.friendli.ai/serverless/v1", - name: "Friendli", - doc: "https://friendli.ai/docs/guides/serverless_endpoints/introduction", - models: { - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-29", - last_updated: "2026-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.8 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 202752 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.5 }, - limit: { context: 202752, output: 202752 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "meta-llama/Llama-3.1-8B-Instruct": { - id: "meta-llama/Llama-3.1-8B-Instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-08-01", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 196608, output: 196608 }, - }, - }, - }, - cortecs: { - id: "cortecs", - env: ["CORTECS_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.cortecs.ai/v1", - name: "Cortecs", - doc: "https://api.cortecs.ai/v1/models", - models: { - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax-m2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.47, output: 1.4 }, - limit: { context: 202752, output: 196072 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.09, output: 5.43 }, - limit: { context: 200000, output: 200000 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.062, output: 0.408 }, - limit: { context: 131000, output: 131000 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.76 }, - limit: { context: 256000, output: 256000 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.551, output: 1.654 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.23 }, - limit: { context: 198000, output: 198000 }, - }, - "claude-opus4-7": { - id: "claude-opus4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5.6, output: 27.99, cache_read: 0.56, cache_write: 6.99 }, - limit: { context: 1000000, output: 128000 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM 5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.08, output: 3.44 }, - limit: { context: 202752, output: 202752 }, - }, - "nova-pro-v1": { - id: "nova-pro-v1", - name: "Nova Pro 1.0", - family: "nova-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.016, output: 4.061 }, - limit: { context: 300000, output: 5000 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262000, output: 262000 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.099, output: 0.33 }, - limit: { context: 16384, output: 16384 }, - }, - "codestral-2508": { - id: "codestral-2508", - name: "Codestral 2508", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9, cache_read: 0.03 }, - limit: { context: 256000, output: 256000 }, - }, - "claude-4-5-sonnet": { - id: "claude-4-5-sonnet", - name: "Claude 4.5 Sonnet", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.259, output: 16.296 }, - limit: { context: 200000, output: 200000 }, - }, - "kimi-k2-instruct": { - id: "kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-07-11", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.551, output: 2.646 }, - limit: { context: 131000, output: 131000 }, - }, - "nemotron-3-super-120b-a12b": { - id: "nemotron-3-super-120b-a12b", - name: "Nemotron 3 Super 120B A12B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.266, output: 0.799 }, - limit: { context: 262144, output: 262144 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-11", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.39, output: 1.57 }, - limit: { context: 400000, output: 400000 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.654, output: 11.024 }, - limit: { context: 1048576, output: 65535 }, - }, - "claude-opus4-6": { - id: "claude-opus4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5.98, output: 29.89 }, - limit: { context: 1000000, output: 1000000 }, - }, - "devstral-small-2512": { - id: "devstral-small-2512", - name: "Devstral Small 2 2512", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262000, output: 262000 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.34, output: 1.34 }, - limit: { context: 196000, output: 196000 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-14", - last_updated: "2026-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.31, output: 4.1, cache_read: 0.24 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-29", - last_updated: "2025-07-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.67, output: 2.46 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-opus4-5": { - id: "claude-opus4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5.98, output: 29.89 }, - limit: { context: 200000, output: 200000 }, - }, - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.307, output: 16.536 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-11", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.164, output: 1.311 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-01", - last_updated: "2025-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 1.34 }, - limit: { context: 131072, output: 131072 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.81, output: 3.54, cache_read: 0.2 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next 80B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-02-04", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.158, output: 0.84 }, - limit: { context: 256000, output: 65536 }, - }, - "claude-4-6-sonnet": { - id: "claude-4-6-sonnet", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3.59, output: 17.92 }, - limit: { context: 1000000, output: 1000000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.441, output: 1.984 }, - limit: { context: 262000, output: 262000 }, - }, - "mixtral-8x7B-instruct-v0.1": { - id: "mixtral-8x7B-instruct-v0.1", - name: "Mixtral 8x7B Instruct v0.1", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.438, output: 0.68 }, - limit: { context: 32000, output: 32000 }, - }, - "hermes-4-70b": { - id: "hermes-4-70b", - name: "Hermes 4 70B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.116, output: 0.358 }, - limit: { context: 128000, output: 128000 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.32, output: 1.18 }, - limit: { context: 196608, output: 196608 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.266, output: 0.444 }, - limit: { context: 163840, output: 163840 }, - }, - "intellect-3": { - id: "intellect-3", - name: "INTELLECT 3", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.219, output: 1.202 }, - limit: { context: 128000, output: 128000 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-08", - last_updated: "2025-08-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.53 }, - limit: { context: 203000, output: 203000 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT Oss 120b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-01", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen-2.5-72b-instruct": { - id: "qwen-2.5-72b-instruct", - name: "Qwen2.5 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.062, output: 0.231 }, - limit: { context: 33000, output: 33000 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek R1 0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.585, output: 2.307 }, - limit: { context: 164000, output: 164000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT 4.1", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.354, output: 9.417 }, - limit: { context: 1047576, output: 32768 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.656, output: 2.731 }, - limit: { context: 262000, output: 262000 }, - }, - "llama-3.1-405b-instruct": { - id: "llama-3.1-405b-instruct", - name: "Llama 3.1 405B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3.5-122b-a10b": { - id: "qwen3.5-122b-a10b", - name: "Qwen3.5 122B A10B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.444, output: 3.106 }, - limit: { context: 262144, output: 262144 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.089, output: 0.275 }, - limit: { context: 131000, output: 131000 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3 2512", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5, cache_read: 0.05 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3.5-397b-a17b": { - id: "qwen3.5-397b-a17b", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 250000, output: 250000 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.133, output: 0.266, cache_read: 0.028 }, - limit: { context: 1048576, output: 384000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.553, output: 3.106, cache_read: 0.145 }, - limit: { context: 1048576, output: 384000 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.053, output: 0.222 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - siliconflow: { - id: "siliconflow", - env: ["SILICONFLOW_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.siliconflow.com/v1", - name: "SiliconFlow", - doc: "https://cloud.siliconflow.com/models", - models: { - "nex-agi/DeepSeek-V3.1-Nex-N1": { - id: "nex-agi/DeepSeek-V3.1-Nex-N1", - name: "nex-agi/DeepSeek-V3.1-Nex-N1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - id: "Qwen/Qwen2.5-VL-72B-Instruct", - name: "Qwen/Qwen2.5-VL-72B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen3-VL-32B-Thinking": { - id: "Qwen/Qwen3-VL-32B-Thinking", - name: "Qwen/Qwen3-VL-32B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen/Qwen3-30B-A3B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 131000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Thinking": { - id: "Qwen/Qwen3-VL-235B-A22B-Thinking", - name: "Qwen/Qwen3-VL-235B-A22B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.45, output: 3.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-7B-Instruct": { - id: "Qwen/Qwen2.5-7B-Instruct", - name: "Qwen/Qwen2.5-7B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - id: "Qwen/Qwen2.5-Coder-32B-Instruct", - name: "Qwen/Qwen2.5-Coder-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-11-11", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - id: "Qwen/Qwen3-VL-30B-A3B-Instruct", - name: "Qwen/Qwen3-VL-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-05", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/QwQ-32B": { - id: "Qwen/QwQ-32B", - name: "Qwen/QwQ-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-06", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.58 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-235B-A22B": { - id: "Qwen/Qwen3-235B-A22B", - name: "Qwen/Qwen3-235B-A22B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.35, output: 1.42 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Captioner": { - id: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - name: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-VL-8B-Thinking": { - id: "Qwen/Qwen3-VL-8B-Thinking", - name: "Qwen/Qwen3-VL-8B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-VL-7B-Instruct": { - id: "Qwen/Qwen2.5-VL-7B-Instruct", - name: "Qwen/Qwen2.5-VL-7B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-28", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - id: "Qwen/Qwen3-Next-80B-A3B-Instruct", - name: "Qwen/Qwen3-Next-80B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.4 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-8B": { - id: "Qwen/Qwen3-8B", - name: "Qwen/Qwen3-8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen/Qwen3-30B-A3B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen/Qwen3-235B-A22B-Instruct-2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-32B": { - id: "Qwen/Qwen3-32B", - name: "Qwen/Qwen3-32B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-01", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - name: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen3-14B": { - id: "Qwen/Qwen3-14B", - name: "Qwen/Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen2.5-72B-Instruct-128K": { - id: "Qwen/Qwen2.5-72B-Instruct-128K", - name: "Qwen/Qwen2.5-72B-Instruct-128K", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 131000, output: 4000 }, - }, - "Qwen/Qwen2.5-32B-Instruct": { - id: "Qwen/Qwen2.5-32B-Instruct", - name: "Qwen/Qwen2.5-32B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen/Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Omni-30B-A3B-Thinking": { - id: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - name: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4 }, - limit: { context: 66000, output: 66000 }, - }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - id: "Qwen/Qwen2.5-VL-32B-Instruct", - name: "Qwen/Qwen2.5-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-24", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 131000, output: 131000 }, - }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - id: "Qwen/Qwen3-Next-80B-A3B-Thinking", - name: "Qwen/Qwen3-Next-80B-A3B-Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct", - name: "Qwen/Qwen3-VL-235B-A22B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-14B-Instruct": { - id: "Qwen/Qwen2.5-14B-Instruct", - name: "Qwen/Qwen2.5-14B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 33000, output: 4000 }, - }, - "Qwen/Qwen3-VL-30B-A3B-Thinking": { - id: "Qwen/Qwen3-VL-30B-A3B-Thinking", - name: "Qwen/Qwen3-VL-30B-A3B-Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-11", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-32B-Instruct": { - id: "Qwen/Qwen3-VL-32B-Instruct", - name: "Qwen/Qwen3-VL-32B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-21", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-VL-8B-Instruct": { - id: "Qwen/Qwen3-VL-8B-Instruct", - name: "Qwen/Qwen3-VL-8B-Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.68 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - name: "Qwen/Qwen3-Coder-480B-A35B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 262000, output: 262000 }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - id: "Qwen/Qwen2.5-72B-Instruct", - name: "Qwen/Qwen2.5-72B-Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.59, output: 0.59 }, - limit: { context: 33000, output: 4000 }, - }, - "stepfun-ai/Step-3.5-Flash": { - id: "stepfun-ai/Step-3.5-Flash", - name: "stepfun-ai/Step-3.5-Flash", - family: "step", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 262000, output: 262000 }, - }, - "zai-org/GLM-4.5": { - id: "zai-org/GLM-4.5", - name: "zai-org/GLM-4.5", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-5V-Turbo": { - id: "zai-org/GLM-5V-Turbo", - name: "zai-org/GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "zai-org/GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "zai-org/GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4, cache_write: 0 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.5-Air": { - id: "zai-org/GLM-4.5-Air", - name: "zai-org/GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-5": { - id: "zai-org/GLM-5", - name: "zai-org/GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.6V": { - id: "zai-org/GLM-4.6V", - name: "zai-org/GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-12-07", - last_updated: "2025-12-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131000, output: 131000 }, - }, - "zai-org/GLM-4.6": { - id: "zai-org/GLM-4.6", - name: "zai-org/GLM-4.6", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.9 }, - limit: { context: 205000, output: 205000 }, - }, - "zai-org/GLM-4.5V": { - id: "zai-org/GLM-4.5V", - name: "zai-org/GLM-4.5V", - family: "glm", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.86 }, - limit: { context: 66000, output: 66000 }, - }, - "meta-llama/Meta-Llama-3.1-8B-Instruct": { - id: "meta-llama/Meta-Llama-3.1-8B-Instruct", - name: "meta-llama/Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-23", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.06 }, - limit: { context: 33000, output: 4000 }, - }, - "inclusionAI/Ring-flash-2.0": { - id: "inclusionAI/Ring-flash-2.0", - name: "inclusionAI/Ring-flash-2.0", - family: "ring", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-mini-2.0": { - id: "inclusionAI/Ling-mini-2.0", - name: "inclusionAI/Ling-mini-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.28 }, - limit: { context: 131000, output: 131000 }, - }, - "inclusionAI/Ling-flash-2.0": { - id: "inclusionAI/Ling-flash-2.0", - name: "inclusionAI/Ling-flash-2.0", - family: "ling", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "tencent/Hunyuan-A13B-Instruct": { - id: "tencent/Hunyuan-A13B-Instruct", - name: "tencent/Hunyuan-A13B-Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-30", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "tencent/Hunyuan-MT-7B": { - id: "tencent/Hunyuan-MT-7B", - name: "tencent/Hunyuan-MT-7B", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 33000, output: 33000 }, - }, - "deepseek-ai/DeepSeek-V3.1": { - id: "deepseek-ai/DeepSeek-V3.1", - name: "deepseek-ai/DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-25", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/deepseek-vl2": { - id: "deepseek-ai/deepseek-vl2", - name: "deepseek-ai/deepseek-vl2", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-13", - last_updated: "2025-11-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 4000, output: 4000 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "deepseek-ai/DeepSeek-V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-12-26", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0.18 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "deepseek-ai/DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.18 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { - id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-20", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131000, output: 131000 }, - }, - "deepseek-ai/DeepSeek-V3.2-Exp": { - id: "deepseek-ai/DeepSeek-V3.2-Exp", - name: "deepseek-ai/DeepSeek-V3.2-Exp", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-10", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.41 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.2": { - id: "deepseek-ai/DeepSeek-V3.2", - name: "deepseek-ai/DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2025-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.42 }, - limit: { context: 164000, output: 164000 }, - }, - "deepseek-ai/DeepSeek-V3.1-Terminus": { - id: "deepseek-ai/DeepSeek-V3.1-Terminus", - name: "deepseek-ai/DeepSeek-V3.1-Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 1 }, - limit: { context: 164000, output: 164000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "openai/gpt-oss-20b", - family: "gpt-oss", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.18 }, - limit: { context: 131000, output: 8000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "openai/gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.45 }, - limit: { context: 131000, output: 8000 }, - }, - "baidu/ERNIE-4.5-300B-A47B": { - id: "baidu/ERNIE-4.5-300B-A47B", - name: "baidu/ERNIE-4.5-300B-A47B", - family: "ernie", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-02", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-Z1-9B-0414": { - id: "THUDM/GLM-Z1-9B-0414", - name: "THUDM/GLM-Z1-9B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 131000, output: 131000 }, - }, - "THUDM/GLM-4-9B-0414": { - id: "THUDM/GLM-4-9B-0414", - name: "THUDM/GLM-4-9B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.086, output: 0.086 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-4-32B-0414": { - id: "THUDM/GLM-4-32B-0414", - name: "THUDM/GLM-4-32B-0414", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 33000, output: 33000 }, - }, - "THUDM/GLM-Z1-32B-0414": { - id: "THUDM/GLM-Z1-32B-0414", - name: "THUDM/GLM-Z1-32B-0414", - family: "glm-z", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-18", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.57 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Thinking": { - id: "moonshotai/Kimi-K2-Thinking", - name: "moonshotai/Kimi-K2-Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-07", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.55, output: 2.5 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "moonshotai/Kimi-K2.6", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2-Instruct": { - id: "moonshotai/Kimi-K2-Instruct", - name: "moonshotai/Kimi-K2-Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-13", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.58, output: 2.29 }, - limit: { context: 131000, output: 131000 }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - id: "moonshotai/Kimi-K2-Instruct-0905", - name: "moonshotai/Kimi-K2-Instruct-0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-08", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "moonshotai/Kimi-K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.25 }, - limit: { context: 262000, output: 262000 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMaxAI/MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "MiniMaxAI/MiniMax-M2.1": { - id: "MiniMaxAI/MiniMax-M2.1", - name: "MiniMaxAI/MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 197000, output: 131000 }, - }, - "ByteDance-Seed/Seed-OSS-36B-Instruct": { - id: "ByteDance-Seed/Seed-OSS-36B-Instruct", - name: "ByteDance-Seed/Seed-OSS-36B-Instruct", - family: "seed", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-04", - last_updated: "2025-11-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.57 }, - limit: { context: 262000, output: 262000 }, - }, - }, - }, - vercel: { - id: "vercel", - env: ["AI_GATEWAY_API_KEY"], - npm: "@ai-sdk/gateway", - name: "Vercel AI Gateway", - doc: "https://github.com/vercel/ai/tree/5eb85cc45a259553501f535b8ac79a77d0e79223/packages/gateway", - models: { - "alibaba/qwen3-coder-plus": { - id: "alibaba/qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1000000, output: 1000000 }, - }, - "alibaba/qwen3.6-27b": { - id: "alibaba/qwen3.6-27b", - name: "Qwen 3.6 27B", - family: "qwen3.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 3.5999999999999996 }, - limit: { context: 256000, output: 256000 }, - }, - "alibaba/qwen3-embedding-8b": { - id: "alibaba/qwen3-embedding-8b", - name: "Qwen3 Embedding 8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen-3-30b": { - id: "alibaba/qwen-3-30b", - name: "Qwen3-30B-A3B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.29 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen-3-235b": { - id: "alibaba/qwen-3-235b", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.6 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen3.5-flash": { - id: "alibaba/qwen3.5-flash", - name: "Qwen 3.5 Flash", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.001, cache_write: 0.125 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen3.6-plus": { - id: "alibaba/qwen3.6-plus", - name: "Qwen 3.6 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.09999999999999999, cache_write: 0.625 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen3-max": { - id: "alibaba/qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3-embedding-0.6b": { - id: "alibaba/qwen3-embedding-0.6b", - name: "Qwen3 Embedding 0.6B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.01, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen-3-32b": { - id: "alibaba/qwen-3-32b", - name: "Qwen 3.32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen-3.6-max-preview": { - id: "alibaba/qwen-3.6-max-preview", - name: "Qwen 3.6 Max Preview", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 1.3, output: 7.8, cache_read: 0.26, cache_write: 1.625 }, - limit: { context: 240000, output: 64000 }, - }, - "alibaba/qwen3-next-80b-a3b-thinking": { - id: "alibaba/qwen3-next-80b-a3b-thinking", - name: "Qwen3 Next 80B A3B Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 131072, output: 65536 }, - }, - "alibaba/qwen3-vl-thinking": { - id: "alibaba/qwen3-vl-thinking", - name: "Qwen3 VL Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 8.4 }, - limit: { context: 131072, output: 129024 }, - }, - "alibaba/qwen3-235b-a22b-thinking": { - id: "alibaba/qwen3-235b-a22b-thinking", - name: "Qwen3 235B A22B Thinking 2507", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.9 }, - limit: { context: 262114, output: 262114 }, - }, - "alibaba/qwen3-next-80b-a3b-instruct": { - id: "alibaba/qwen3-next-80b-a3b-instruct", - name: "Qwen3 Next 80B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-12", - last_updated: "2025-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 1.1 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3-coder-next": { - id: "alibaba/qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-07-22", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 256000, output: 256000 }, - }, - "alibaba/qwen3-embedding-4b": { - id: "alibaba/qwen3-embedding-4b", - name: "Qwen3 Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 32768, output: 32768 }, - }, - "alibaba/qwen3-max-thinking": { - id: "alibaba/qwen3-max-thinking", - name: "Qwen 3 Max Thinking", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 256000, output: 65536 }, - }, - "alibaba/qwen3-vl-235b-a22b-instruct": { - id: "alibaba/qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-09-24", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 1.5999999999999999 }, - limit: { context: 131072, output: 129024 }, - }, - "alibaba/qwen3-coder": { - id: "alibaba/qwen3-coder", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.38, output: 1.53 }, - limit: { context: 262144, output: 66536 }, - }, - "alibaba/qwen3-max-preview": { - id: "alibaba/qwen3-max-preview", - name: "Qwen3 Max Preview", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6, cache_read: 0.24 }, - limit: { context: 262144, output: 32768 }, - }, - "alibaba/qwen3.5-plus": { - id: "alibaba/qwen3.5-plus", - name: "Qwen 3.5 Plus", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2.4, cache_read: 0.04, cache_write: 0.5 }, - limit: { context: 1000000, output: 64000 }, - }, - "alibaba/qwen-3-14b": { - id: "alibaba/qwen-3-14b", - name: "Qwen3-14B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 40960, output: 16384 }, - }, - "alibaba/qwen3-vl-instruct": { - id: "alibaba/qwen3-vl-instruct", - name: "Qwen3 VL Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-24", - last_updated: "2025-09-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8 }, - limit: { context: 131072, output: 129024 }, - }, - "alibaba/qwen3-coder-30b-a3b": { - id: "alibaba/qwen3-coder-30b-a3b", - name: "Qwen 3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.27 }, - limit: { context: 160000, output: 32768 }, - }, - "perplexity/sonar-pro": { - id: "perplexity/sonar-pro", - name: "Sonar Pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8000 }, - }, - "perplexity/sonar": { - id: "perplexity/sonar", - name: "Sonar", - family: "sonar", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-02", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 127000, output: 8000 }, - }, - "perplexity/sonar-reasoning": { - id: "perplexity/sonar-reasoning", - name: "Sonar Reasoning", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5 }, - limit: { context: 127000, output: 8000 }, - }, - "perplexity/sonar-reasoning-pro": { - id: "perplexity/sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 127000, output: 8000 }, - }, - "deepseek/deepseek-v3.2-thinking": { - id: "deepseek/deepseek-v3.2-thinking", - name: "DeepSeek V3.2 Thinking", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 128000, output: 64000 }, - }, - "deepseek/deepseek-v3.2-exp": { - id: "deepseek/deepseek-v3.2-exp", - name: "DeepSeek V3.2 Exp", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.4 }, - limit: { context: 163840, output: 163840 }, - }, - "deepseek/deepseek-v3.1": { - id: "deepseek/deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1 }, - limit: { context: 163840, output: 128000 }, - }, - "deepseek/deepseek-v4-flash": { - id: "deepseek/deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-23", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek/deepseek-v4-pro": { - id: "deepseek/deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-23", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - }, - "deepseek/deepseek-v3.2": { - id: "deepseek/deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.27, output: 0.4, cache_read: 0.22 }, - limit: { context: 163842, output: 8000 }, - }, - "deepseek/deepseek-v3": { - id: "deepseek/deepseek-v3", - name: "DeepSeek V3 0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.77, output: 0.77 }, - limit: { context: 163840, output: 16384 }, - }, - "deepseek/deepseek-v3.1-terminus": { - id: "deepseek/deepseek-v3.1-terminus", - name: "DeepSeek V3.1 Terminus", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-22", - last_updated: "2025-09-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1 }, - limit: { context: 131072, output: 65536 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 32768 }, - }, - "arcee-ai/trinity-mini": { - id: "arcee-ai/trinity-mini", - name: "Trinity Mini", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2025-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "arcee-ai/trinity-large-thinking": { - id: "arcee-ai/trinity-large-thinking", - name: "Trinity Large Thinking", - family: "trinity", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 0.8999999999999999 }, - limit: { context: 262100, output: 80000 }, - }, - "arcee-ai/trinity-large-preview": { - id: "arcee-ai/trinity-large-preview", - name: "Trinity Large Preview", - family: "trinity", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 131000, output: 131000 }, - }, - "recraft/recraft-v3": { - id: "recraft/recraft-v3", - name: "Recraft V3", - family: "recraft", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "recraft/recraft-v2": { - id: "recraft/recraft-v2", - name: "Recraft V2", - family: "recraft", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "voyage/voyage-3-large": { - id: "voyage/voyage-3-large", - name: "voyage-3-large", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-4-large": { - id: "voyage/voyage-4-large", - name: "voyage-4-large", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-3.5-lite": { - id: "voyage/voyage-3.5-lite", - name: "voyage-3.5-lite", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-code-3": { - id: "voyage/voyage-code-3", - name: "voyage-code-3", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.18, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-finance-2": { - id: "voyage/voyage-finance-2", - name: "voyage-finance-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-4-lite": { - id: "voyage/voyage-4-lite", - name: "voyage-4-lite", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-4": { - id: "voyage/voyage-4", - name: "voyage-4", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-06", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32000, output: 0 }, - }, - "voyage/voyage-code-2": { - id: "voyage/voyage-code-2", - name: "voyage-code-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01", - last_updated: "2024-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-law-2": { - id: "voyage/voyage-law-2", - name: "voyage-law-2", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "voyage/voyage-3.5": { - id: "voyage/voyage-3.5", - name: "voyage-3.5", - family: "voyage", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "morph/morph-v3-large": { - id: "morph/morph-v3-large", - name: "Morph v3 Large", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.9, output: 1.9 }, - limit: { context: 32000, output: 32000 }, - }, - "morph/morph-v3-fast": { - id: "morph/morph-v3-fast", - name: "Morph v3 Fast", - family: "morph", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08-15", - last_updated: "2024-08-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 1.2 }, - limit: { context: 16000, output: 16000 }, - }, - "zai/glm-5v-turbo": { - id: "zai/glm-5v-turbo", - name: "GLM 5V Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 200000, output: 128000 }, - }, - "zai/glm-4.7": { - id: "zai/glm-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.43, output: 1.75, cache_read: 0.08 }, - limit: { context: 202752, output: 120000 }, - }, - "zai/glm-5": { - id: "zai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2 }, - limit: { context: 202800, output: 131072 }, - }, - "zai/glm-4.7-flashx": { - id: "zai/glm-4.7-flashx", - name: "GLM 4.7 FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4, cache_read: 0.01 }, - limit: { context: 200000, output: 128000 }, - }, - "zai/glm-5.1": { - id: "zai/glm-5.1", - name: "GLM 5.1", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.4, output: 4.4, cache_read: 0.26 }, - limit: { context: 202752, output: 202752 }, - }, - "zai/glm-4.6v-flash": { - id: "zai/glm-4.6v-flash", - name: "GLM-4.6V-Flash", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 24000 }, - }, - "zai/glm-4.5": { - id: "zai/glm-4.5", - name: "GLM 4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 131072, output: 131072 }, - }, - "zai/glm-4.5-air": { - id: "zai/glm-4.5-air", - name: "GLM 4.5 Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 128000, output: 96000 }, - }, - "zai/glm-5-turbo": { - id: "zai/glm-5-turbo", - name: "GLM 5 Turbo", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 4, cache_read: 0.24 }, - limit: { context: 202800, output: 131100 }, - }, - "zai/glm-4.5v": { - id: "zai/glm-4.5v", - name: "GLM 4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 66000, output: 66000 }, - }, - "zai/glm-4.6": { - id: "zai/glm-4.6", - name: "GLM 4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 1.8 }, - limit: { context: 200000, output: 96000 }, - }, - "zai/glm-4.6v": { - id: "zai/glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9, cache_read: 0.05 }, - limit: { context: 128000, output: 24000 }, - }, - "zai/glm-4.7-flash": { - id: "zai/glm-4.7-flash", - name: "GLM 4.7 Flash", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.39999999999999997 }, - limit: { context: 200000, output: 131000 }, - }, - "cohere/command-a": { - id: "cohere/command-a", - name: "Command A", - family: "command", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "cohere/embed-v4.0": { - id: "cohere/embed-v4.0", - name: "Embed v4.0", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.12, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "prime-intellect/intellect-3": { - id: "prime-intellect/intellect-3", - name: "INTELLECT 3", - family: "intellect", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-11-26", - last_updated: "2025-11-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 131072, output: 131072 }, - }, - "xai/grok-4.3": { - id: "xai/grok-4.3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-30", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 2.5, cache_read: 0.19999999999999998 }, - limit: { context: 1000000, output: 1000000 }, - }, - "xai/grok-4.20-non-reasoning": { - id: "xai/grok-4.20-non-reasoning", - name: "Grok 4.20 Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-non-reasoning-beta": { - id: "xai/grok-4.20-non-reasoning-beta", - name: "Grok 4.20 Beta Non-Reasoning", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-reasoning": { - id: "xai/grok-4.20-reasoning", - name: "Grok 4.20 Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-imagine-image": { - id: "xai/grok-imagine-image", - name: "Grok Imagine Image", - family: "grok", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "xai/grok-4.20-multi-agent-beta": { - id: "xai/grok-4.20-multi-agent-beta", - name: "Grok 4.20 Multi Agent Beta", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-imagine-image-pro": { - id: "xai/grok-imagine-image-pro", - name: "Grok Imagine Image Pro", - family: "grok", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-01-28", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "xai/grok-4-fast-reasoning": { - id: "xai/grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 256000 }, - }, - "xai/grok-4.1-fast-non-reasoning": { - id: "xai/grok-4.1-fast-non-reasoning", - name: "Grok 4.1 Fast Non-Reasoning", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4.20-reasoning-beta": { - id: "xai/grok-4.20-reasoning-beta", - name: "Grok 4.20 Beta Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.20-multi-agent": { - id: "xai/grok-4.20-multi-agent", - name: "Grok 4.20 Multi-Agent", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.19999999999999998 }, - limit: { context: 2000000, output: 2000000 }, - }, - "xai/grok-4.1-fast-reasoning": { - id: "xai/grok-4.1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-4-fast-non-reasoning": { - id: "xai/grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-2-vision": { - id: "xai/grok-2-vision", - name: "Grok 2 Vision", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 10, cache_read: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "xai/grok-4": { - id: "xai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "xai/grok-code-fast-1": { - id: "xai/grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "xai/grok-3-fast": { - id: "xai/grok-3-fast", - name: "Grok 3 Fast", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 1.25 }, - limit: { context: 131072, output: 8192 }, - }, - "xai/grok-3-mini-fast": { - id: "xai/grok-3-mini-fast", - name: "Grok 3 Mini Fast", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 4, reasoning: 4, cache_read: 0.15 }, - limit: { context: 131072, output: 8192 }, - }, - "nvidia/nemotron-3-super-120b-a12b": { - id: "nvidia/nemotron-3-super-120b-a12b", - name: "NVIDIA Nemotron 3 Super 120B A12B", - family: "nemotron", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.65 }, - limit: { context: 256000, output: 32000 }, - }, - "nvidia/nemotron-3-nano-30b-a3b": { - id: "nvidia/nemotron-3-nano-30b-a3b", - name: "Nemotron 3 Nano 30B A3B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24 }, - limit: { context: 262144, output: 262144 }, - }, - "nvidia/nemotron-nano-12b-v2-vl": { - id: "nvidia/nemotron-nano-12b-v2-vl", - name: "Nvidia Nemotron Nano 12B V2 VL", - family: "nemotron", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12", - last_updated: "2024-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "Nvidia Nemotron Nano 9B V2", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-18", - last_updated: "2025-08-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.16 }, - limit: { context: 131072, output: 131072 }, - }, - "inception/mercury-edit-2": { - id: "inception/mercury-edit-2", - name: "Mercury Edit 2", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-30", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.025 }, - limit: { context: 128000, output: 8192 }, - }, - "inception/mercury-2": { - id: "inception/mercury-2", - name: "Mercury 2", - family: "mercury", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-03-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 0.75, cache_read: 0.024999999999999998 }, - limit: { context: 128000, output: 128000 }, - }, - "inception/mercury-coder-small": { - id: "inception/mercury-coder-small", - name: "Mercury Coder Small Beta", - family: "mercury", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-02-26", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1 }, - limit: { context: 32000, output: 16384 }, - }, - "openai/gpt-5.1-codex-max": { - id: "openai/gpt-5.1-codex-max", - name: "GPT 5.1 Codex Max", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2-chat": { - id: "openai/gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-4o-mini-search-preview": { - id: "openai/gpt-4o-mini-search-preview", - name: "GPT 4o Mini Search Preview", - family: "gpt-mini", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2023-09", - release_date: "2025-01", - last_updated: "2025-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/codex-mini": { - id: "openai/codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.38 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/gpt-5-chat": { - id: "openai/gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.3-chat": { - id: "openai/gpt-5.3-chat", - name: "GPT-5.3 Chat", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.2-pro": { - id: "openai/gpt-5.2-pro", - name: "GPT 5.2 ", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/text-embedding-3-large": { - id: "openai/text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.5": { - id: "openai/gpt-5.5", - name: "GPT 5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5 }, - limit: { context: 1000000, input: 872000, output: 128000 }, - }, - "openai/gpt-5.3-codex": { - id: "openai/gpt-5.3-codex", - name: "GPT 5.3 Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/text-embedding-ada-002": { - id: "openai/text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.2": { - id: "openai/gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.18 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/o3-pro": { - id: "openai/o3-pro", - name: "o3 Pro", - family: "o-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 20, output: 80 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/gpt-5.4-mini": { - id: "openai/gpt-5.4-mini", - name: "GPT 5.4 Mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4-nano": { - id: "openai/gpt-5.4-nano", - name: "GPT 5.4 Nano", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.19999999999999998, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.2-codex": { - id: "openai/gpt-5.2-codex", - name: "GPT-5.2-Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2025-12", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1-codex-mini": { - id: "openai/gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.1-thinking": { - id: "openai/gpt-5.1-thinking", - name: "GPT 5.1 Thinking", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5.4-pro": { - id: "openai/gpt-5.4-pro", - name: "GPT 5.4 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-3.5-turbo": { - id: "openai/gpt-3.5-turbo", - name: "GPT-3.5 Turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-09", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16385, input: 12289, output: 4096 }, - }, - "openai/o3-deep-research": { - id: "openai/o3-deep-research", - name: "o3-deep-research", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-10", - release_date: "2024-06-26", - last_updated: "2024-06-26", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 40, cache_read: 2.5 }, - limit: { context: 200000, input: 100000, output: 100000 }, - }, - "openai/text-embedding-3-small": { - id: "openai/text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, input: 6656, output: 1536 }, - }, - "openai/gpt-5.4": { - id: "openai/gpt-5.4", - name: "GPT 5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-05", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3 }, - limit: { context: 131072, input: 98304, output: 32768 }, - }, - "openai/gpt-5-pro": { - id: "openai/gpt-5-pro", - name: "GPT-5 pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 128000, output: 272000 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "gpt-oss-safeguard-20b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.08, output: 0.3, cache_read: 0.04 }, - limit: { context: 131072, input: 65536, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-5.5-pro": { - id: "openai/gpt-5.5-pro", - name: "GPT 5.5 Pro", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1000000, input: 872000, output: 128000 }, - }, - "openai/gpt-3.5-turbo-instruct": { - id: "openai/gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-09", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 8192, input: 4096, output: 4096 }, - }, - "openai/gpt-5.1-instant": { - id: "openai/gpt-5.1-instant", - name: "GPT-5.1 Instant", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, input: 111616, output: 16384 }, - }, - "openai/gpt-5.1-codex": { - id: "openai/gpt-5.1-codex", - name: "GPT-5.1-Codex", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o3": { - id: "openai/o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "openai/gpt-5-codex": { - id: "openai/gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1": { - id: "openai/o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4-turbo": { - id: "openai/gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "amazon/titan-embed-text-v2": { - id: "amazon/titan-embed-text-v2", - name: "Titan Text Embeddings V2", - family: "titan-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-04", - last_updated: "2024-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "amazon/nova-2-lite": { - id: "amazon/nova-2-lite", - name: "Nova 2 Lite", - family: "nova", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-01", - last_updated: "2024-12-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 1000000, output: 1000000 }, - }, - "amazon/nova-pro": { - id: "amazon/nova-pro", - name: "Nova Pro", - family: "nova-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2, cache_read: 0.2 }, - limit: { context: 300000, output: 8192 }, - }, - "amazon/nova-lite": { - id: "amazon/nova-lite", - name: "Nova Lite", - family: "nova-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.06, output: 0.24, cache_read: 0.015 }, - limit: { context: 300000, output: 8192 }, - }, - "amazon/nova-micro": { - id: "amazon/nova-micro", - name: "Nova Micro", - family: "nova-micro", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-03", - last_updated: "2024-12-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.035, output: 0.14, cache_read: 0.00875 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral/mistral-nemo": { - id: "mistral/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.17 }, - limit: { context: 60288, output: 16000 }, - }, - "mistral/ministral-14b": { - id: "mistral/ministral-14b", - name: "Ministral 14B", - family: "ministral", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/codestral-embed": { - id: "mistral/codestral-embed", - name: "Codestral Embed", - family: "codestral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "mistral/mistral-medium": { - id: "mistral/mistral-medium", - name: "Mistral Medium 3.1", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 64000 }, - }, - "mistral/mistral-embed": { - id: "mistral/mistral-embed", - name: "Mistral Embed", - family: "mistral-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "mistral/devstral-2": { - id: "mistral/devstral-2", - name: "Devstral 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "mistral/mistral-large-3": { - id: "mistral/mistral-large-3", - name: "Mistral Large 3", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/devstral-small-2": { - id: "mistral/devstral-small-2", - name: "Devstral Small 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 256000 }, - }, - "mistral/devstral-small": { - id: "mistral/devstral-small", - name: "Devstral Small 1.1", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 64000 }, - }, - "mistral/ministral-8b": { - id: "mistral/ministral-8b", - name: "Ministral 8B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/magistral-medium": { - id: "mistral/magistral-medium", - name: "Magistral Medium (latest)", - family: "magistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 5 }, - limit: { context: 128000, output: 16384 }, - }, - "mistral/mistral-small": { - id: "mistral/mistral-small", - name: "Mistral Small (latest)", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2026-03-16", - last_updated: "2026-03-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 256000, output: 256000 }, - }, - "mistral/magistral-small": { - id: "mistral/magistral-small", - name: "Magistral Small", - family: "magistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-06", - release_date: "2025-03-17", - last_updated: "2025-03-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/pixtral-12b": { - id: "mistral/pixtral-12b", - name: "Pixtral 12B", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09-01", - last_updated: "2024-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/mixtral-8x22b-instruct": { - id: "mistral/mixtral-8x22b-instruct", - name: "Mixtral 8x22B", - family: "mixtral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-17", - last_updated: "2024-04-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 64000, output: 64000 }, - }, - "mistral/pixtral-large": { - id: "mistral/pixtral-large", - name: "Pixtral Large (latest)", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/ministral-3b": { - id: "mistral/ministral-3b", - name: "Ministral 3B (latest)", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 128000 }, - }, - "mistral/codestral": { - id: "mistral/codestral", - name: "Codestral (latest)", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-05-29", - last_updated: "2025-01-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 4096 }, - }, - "meta/llama-3.2-1b": { - id: "meta/llama-3.2-1b", - name: "Llama 3.2 1B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.1-8b": { - id: "meta/llama-3.1-8b", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 131072, output: 16384 }, - }, - "meta/llama-3.2-90b": { - id: "meta/llama-3.2-90b", - name: "Llama 3.2 90B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-3b": { - id: "meta/llama-3.2-3b", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-11b": { - id: "meta/llama-3.2-11b", - name: "Llama 3.2 11B Vision Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.16, output: 0.16 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.1-70b": { - id: "meta/llama-3.1-70b", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 0.4 }, - limit: { context: 131072, output: 16384 }, - }, - "meta/llama-3.3-70b": { - id: "meta/llama-3.3-70b", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-maverick": { - id: "meta/llama-4-maverick", - name: "Llama-4-Maverick-17B-128E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "meta/llama-4-scout": { - id: "meta/llama-4-scout", - name: "Llama-4-Scout-17B-16E-Instruct-FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "vercel/v0-1.5-md": { - id: "vercel/v0-1.5-md", - name: "v0-1.5-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-09", - last_updated: "2025-06-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "vercel/v0-1.0-md": { - id: "vercel/v0-1.0-md", - name: "v0-1.0-md", - family: "v0", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 128000, output: 32000 }, - }, - "minimax/minimax-m2.7": { - id: "minimax/minimax-m2.7", - name: "Minimax M2.7", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131000 }, - }, - "minimax/minimax-m2.7-highspeed": { - id: "minimax/minimax-m2.7-highspeed", - name: "MiniMax M2.7 High Speed", - family: "minimax", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131100 }, - }, - "minimax/minimax-m2": { - id: "minimax/minimax-m2", - name: "MiniMax M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 1.15, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 262114, output: 262114 }, - }, - "minimax/minimax-m2.1": { - id: "minimax/minimax-m2.1", - name: "MiniMax M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.1-lightning": { - id: "minimax/minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.4, cache_read: 0.03, cache_write: 0.38 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131000 }, - }, - "minimax/minimax-m2.5-highspeed": { - id: "minimax/minimax-m2.5-highspeed", - name: "MiniMax M2.5 High Speed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.4, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 0, output: 0 }, - }, - "kwaipilot/kat-coder-pro-v1": { - id: "kwaipilot/kat-coder-pro-v1", - name: "KAT-Coder-Pro V1", - family: "kat-coder", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10-24", - last_updated: "2025-10-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 256000, output: 32000 }, - }, - "kwaipilot/kat-coder-pro-v2": { - id: "kwaipilot/kat-coder-pro-v2", - name: "Kat Coder Pro V2", - family: "kat-coder", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 256000, output: 256000 }, - }, - "google/gemini-2.5-flash-lite-preview-09-2025": { - id: "google/gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-3.1-flash-lite-preview": { - id: "google/gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-03", - last_updated: "2026-03-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1000000, output: 65000 }, - }, - "google/gemini-3-pro-image": { - id: "google/gemini-3-pro-image", - name: "Nano Banana Pro (Gemini 3 Pro Image)", - family: "gemini-pro", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-03", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 2, output: 120 }, - limit: { context: 65536, output: 32768 }, - }, - "google/gemini-3.1-pro-preview": { - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-19", - last_updated: "2026-02-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/gemini-3-pro-preview": { - id: "google/gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1000000, output: 64000 }, - }, - "google/imagen-4.0-ultra-generate-001": { - id: "google/imagen-4.0-ultra-generate-001", - name: "Imagen 4 Ultra", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-24", - last_updated: "2025-05-24", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-embedding-001": { - id: "google/gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/gemma-4-31b-it": { - id: "google/gemma-4-31b-it", - name: "Gemma 4 31B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.39999999999999997 }, - limit: { context: 262144, output: 131072 }, - }, - "google/gemini-2.5-flash-image": { - id: "google/gemini-2.5-flash-image", - name: "Nano Banana (Gemini 2.5 Flash Image)", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/text-embedding-005": { - id: "google/text-embedding-005", - name: "Text Embedding 005", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-08", - last_updated: "2024-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/text-multilingual-embedding-002": { - id: "google/text-multilingual-embedding-002", - name: "Text Multilingual Embedding 002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-03", - last_updated: "2024-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.03, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "google/gemini-3.1-flash-image-preview": { - id: "google/gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image Preview (Nano Banana 2)", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-02-26", - last_updated: "2026-03-06", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.5, output: 3 }, - limit: { context: 131072, output: 32768 }, - }, - "google/gemini-3.1-flash-lite": { - id: "google/gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-05-07", - last_updated: "2026-05-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.03 }, - limit: { context: 1000000, output: 65000 }, - }, - "google/gemini-3-flash": { - id: "google/gemini-3-flash", - name: "Gemini 3 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 3, cache_read: 0.05 }, - limit: { context: 1000000, output: 64000 }, - }, - "google/imagen-4.0-generate-001": { - id: "google/imagen-4.0-generate-001", - name: "Imagen 4", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-preview-09-2025": { - id: "google/gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-embedding-2": { - id: "google/gemini-embedding-2", - name: "Gemini Embedding 2", - family: "gemini-embedding", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 0, output: 0 }, - }, - "google/gemma-4-26b-a4b-it": { - id: "google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-03", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0.39999999999999997 }, - limit: { context: 262144, output: 131072 }, - }, - "google/imagen-4.0-fast-generate-001": { - id: "google/imagen-4.0-fast-generate-001", - name: "Imagen 4 Fast", - family: "imagen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 480, output: 0 }, - }, - "google/gemini-2.5-flash-lite": { - id: "google/gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.01 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash-image-preview": { - id: "google/gemini-2.5-flash-image-preview", - name: "Nano Banana Preview (Gemini 2.5 Flash Image Preview)", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-03-20", - modalities: { input: ["text"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5 }, - limit: { context: 32768, output: 32768 }, - }, - "google/gemini-2.0-flash-lite": { - id: "google/gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.0-flash": { - id: "google/gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.4, output: 10 }, - limit: { context: 256000, output: 16384 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-26", - last_updated: "2026-01-26", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262114, output: 262114 }, - }, - "moonshotai/kimi-k2-0905": { - id: "moonshotai/kimi-k2-0905", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 131072, output: 16384 }, - }, - "moonshotai/kimi-k2.6": { - id: "moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262000, output: 262000 }, - }, - "moonshotai/kimi-k2-thinking": { - id: "moonshotai/kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.47, output: 2, cache_read: 0.14 }, - limit: { context: 216144, output: 216144 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "interfaze/interfaze-beta": { - id: "interfaze/interfaze-beta", - name: "Interfaze Beta", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2025-10-07", - last_updated: "2026-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 3.5 }, - limit: { context: 1000000, output: 32000 }, - }, - "anthropic/claude-3.5-sonnet-20240620": { - id: "anthropic/claude-3.5-sonnet-20240620", - name: "Claude 3.5 Sonnet (2024-06-20)", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-opus-4.6": { - id: "anthropic/claude-opus-4.6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02", - last_updated: "2026-02", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.7": { - id: "anthropic/claude-opus-4.7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-opus-4.5": { - id: "anthropic/claude-opus-4.5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 18.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-haiku-4.5": { - id: "anthropic/claude-haiku-4.5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4.6": { - id: "anthropic/claude-sonnet-4.6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 1000000, output: 128000 }, - }, - "anthropic/claude-3-opus": { - id: "anthropic/claude-3-opus", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "anthropic/claude-sonnet-4.5": { - id: "anthropic/claude-sonnet-4.5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "anthropic/claude-3.7-sonnet": { - id: "anthropic/claude-3.7-sonnet", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "xiaomi/mimo-v2.5-pro": { - id: "xiaomi/mimo-v2.5-pro", - name: "MiMo V2.5 Pro", - family: "mimo-v2.5-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-05-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.19999999999999998 }, - limit: { context: 1050000, output: 131000 }, - }, - "xiaomi/mimo-v2.5": { - id: "xiaomi/mimo-v2.5", - name: "MiMo M2.5", - family: "mimo-v2.5", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-22", - last_updated: "2026-05-01", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.39999999999999997, output: 2, cache_read: 0.08 }, - limit: { context: 1050000, output: 131100 }, - }, - "xiaomi/mimo-v2-pro": { - id: "xiaomi/mimo-v2-pro", - name: "MiMo V2 Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.19999999999999998 }, - limit: { context: 1000000, output: 128000 }, - }, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "MiMo V2 Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.29 }, - limit: { context: 262144, output: 32000 }, - }, - "bytedance/seed-1.6": { - id: "bytedance/seed-1.6", - name: "Seed 1.6", - family: "seed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 32000 }, - }, - "bytedance/seed-1.8": { - id: "bytedance/seed-1.8", - name: "Seed 1.8", - family: "seed", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-10", - last_updated: "2025-10", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 64000 }, - }, - "meituan/longcat-flash-chat": { - id: "meituan/longcat-flash-chat", - name: "LongCat Flash Chat", - family: "longcat", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-08-30", - last_updated: "2025-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 128000, output: 8192 }, - }, - "meituan/longcat-flash-thinking": { - id: "meituan/longcat-flash-thinking", - name: "LongCat Flash Thinking", - family: "longcat", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 1.5 }, - limit: { context: 128000, output: 8192 }, - }, - "meituan/longcat-flash-thinking-2601": { - id: "meituan/longcat-flash-thinking-2601", - name: "LongCat Flash Thinking 2601", - family: "longcat", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - release_date: "2026-03-13", - last_updated: "2026-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - limit: { context: 32768, output: 32768 }, - }, - "bfl/flux-pro-1.0-fill": { - id: "bfl/flux-pro-1.0-fill", - name: "FLUX.1 Fill [pro]", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-pro-1.1": { - id: "bfl/flux-pro-1.1", - name: "FLUX1.1 [pro]", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-10", - last_updated: "2024-10", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-kontext-pro": { - id: "bfl/flux-kontext-pro", - name: "FLUX.1 Kontext Pro", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-kontext-max": { - id: "bfl/flux-kontext-max", - name: "FLUX.1 Kontext Max", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-06", - last_updated: "2025-06", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - "bfl/flux-pro-1.1-ultra": { - id: "bfl/flux-pro-1.1-ultra", - name: "FLUX1.1 [pro] Ultra", - family: "flux", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2024-11", - last_updated: "2024-11", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - limit: { context: 512, output: 0 }, - }, - }, - }, - minimax: { - id: "minimax", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimax.io/anthropic/v1", - name: "MiniMax (minimax.io)", - doc: "https://platform.minimax.io/docs/guides/quickstart", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - llmgateway: { - id: "llmgateway", - env: ["LLMGATEWAY_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.llmgateway.io/v1", - name: "LLM Gateway", - doc: "https://llmgateway.io/docs", - models: { - "gpt-4o-mini-search-preview": { - id: "gpt-4o-mini-search-preview", - name: "GPT-4o Mini Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 16384 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "qwen3-235b-a22b-instruct-2507": { - id: "qwen3-235b-a22b-instruct-2507", - name: "Qwen3 235B A22B Instruct (2507)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "llama-4-scout": { - id: "llama-4-scout", - name: "Llama 4 Scout", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.18, output: 0.59 }, - limit: { context: 32768, output: 16384 }, - status: "beta", - }, - "hermes-2-pro-llama-3-8b": { - id: "hermes-2-pro-llama-3-8b", - name: "Hermes 2 Pro Llama 3 8B", - family: "hermes", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-05-27", - last_updated: "2024-05-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.14 }, - limit: { context: 8192, output: 8192 }, - }, - "qwen-coder-plus": { - id: "qwen-coder-plus", - name: "Qwen Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1 }, - limit: { context: 131072, output: 8192 }, - }, - auto: { - id: "auto", - name: "Auto Route", - family: "auto", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "glm-4.6v-flashx": { - id: "glm-4.6v-flashx", - name: "GLM-4.6V FlashX", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.4, cache_read: 0 }, - limit: { context: 128000, output: 16000 }, - }, - "gemma-2-27b-it-together": { - id: "gemma-2-27b-it-together", - name: "Gemma 2 27B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.08 }, - limit: { context: 8192, output: 16384 }, - }, - "codestral-2508": { - id: "codestral-2508", - name: "Codestral", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 16384 }, - }, - "gemma-3-1b-it": { - id: "gemma-3-1b-it", - name: "Gemma 3 1B IT", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.3 }, - limit: { context: 1000000, output: 16384 }, - }, - "glm-4-32b-0414-128k": { - id: "glm-4-32b-0414-128k", - name: "GLM-4 32B (0414-128k)", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 128000, output: 16384 }, - }, - "seed-1-6-flash-250715": { - id: "seed-1-6-flash-250715", - name: "Seed 1.6 Flash (250715)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-26", - last_updated: "2025-07-26", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.3, cache_read: 0.01 }, - limit: { context: 256000, output: 8192 }, - }, - "seed-1-6-250615": { - id: "seed-1-6-250615", - name: "Seed 1.6 (250615)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-06-25", - last_updated: "2025-06-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 8192 }, - }, - "qwen3-vl-235b-a22b-thinking": { - id: "qwen3-vl-235b-a22b-thinking", - name: "Qwen3 VL 235B A22B Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-30b-a3b-thinking": { - id: "qwen3-vl-30b-a3b-thinking", - name: "Qwen3 VL 30B A3B Thinking", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-02", - last_updated: "2025-10-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen2-5-vl-32b-instruct": { - id: "qwen2-5-vl-32b-instruct", - name: "Qwen2.5 VL 32B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-03-15", - last_updated: "2025-03-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.3 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen3-vl-8b-instruct": { - id: "qwen3-vl-8b-instruct", - name: "Qwen3 VL 8B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-08-19", - last_updated: "2025-08-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "claude-3-7-sonnet": { - id: "claude-3-7-sonnet", - name: "Claude 3.7 Sonnet", - family: "claude", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-02-24", - last_updated: "2025-02-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3 }, - limit: { context: 200000, output: 8192 }, - }, - "gemini-pro-latest": { - id: "gemini-pro-latest", - name: "Gemini Pro Latest", - family: "gemini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-27", - last_updated: "2026-02-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-5-haiku": { - id: "claude-3-5-haiku", - name: "Claude 3.5 Haiku", - family: "claude", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08 }, - limit: { context: 200000, output: 8192 }, - status: "deprecated", - }, - "qwen-max-latest": { - id: "qwen-max-latest", - name: "Qwen Max Latest", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 32768, output: 8192 }, - }, - "glm-4.6v-flash": { - id: "glm-4.6v-flash", - name: "GLM-4.6V Flash", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16000 }, - status: "beta", - }, - "qwen3-30b-a3b-instruct-2507": { - id: "qwen3-30b-a3b-instruct-2507", - name: "Qwen3 30B A3B Instruct (2507)", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "minimax-text-01": { - id: "minimax-text-01", - name: "MiniMax Text 01", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-01-15", - last_updated: "2025-01-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1 }, - limit: { context: 1000000, output: 131072 }, - }, - "qwen3-32b-fp8": { - id: "qwen3-32b-fp8", - name: "Qwen3 32B FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "llama-4-scout-17b-instruct": { - id: "llama-4-scout-17b-instruct", - name: "Llama 4 Scout 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.66 }, - limit: { context: 8192, output: 2048 }, - }, - "qwen3-4b-fp8": { - id: "qwen3-4b-fp8", - name: "Qwen3 4B FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 131072, output: 8192 }, - }, - "ministral-8b-2512": { - id: "ministral-8b-2512", - name: "Ministral 8B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 262144, output: 8192 }, - }, - "gemma-3-27b": { - id: "gemma-3-27b", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.27 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-flash": { - id: "qwen3-vl-flash", - name: "Qwen3 VL Flash", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-09", - last_updated: "2025-10-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 1000000, output: 32000 }, - }, - "llama-3.1-70b-instruct": { - id: "llama-3.1-70b-instruct", - name: "Llama 3.1 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 2048 }, - status: "beta", - }, - "seed-1-8-251228": { - id: "seed-1-8-251228", - name: "Seed 1.8 (251228)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-18", - last_updated: "2025-12-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 8192 }, - }, - "qwen3-235b-a22b-thinking-2507": { - id: "qwen3-235b-a22b-thinking-2507", - name: "Qwen3 235B A22B Thinking (2507)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "seed-1-6-250915": { - id: "seed-1-6-250915", - name: "Seed 1.6 (250915)", - family: "seed", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 2, cache_read: 0.05 }, - limit: { context: 256000, output: 8192 }, - }, - "glm-4.5-x": { - id: "glm-4.5-x", - name: "GLM-4.5 X", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2.2, output: 8.9, cache_read: 0.45 }, - limit: { context: 128000, output: 16384 }, - status: "beta", - }, - "qwen3-30b-a3b-thinking-2507": { - id: "qwen3-30b-a3b-thinking-2507", - name: "Qwen3 30B A3B Thinking (2507)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-08", - last_updated: "2025-07-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast Reasoning", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.11 }, - limit: { context: 128000, output: 32768 }, - }, - "ministral-3b-2512": { - id: "ministral-3b-2512", - name: "Ministral 3B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "qwen-plus-latest": { - id: "qwen-plus-latest", - name: "Qwen Plus Latest", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-01-25", - last_updated: "2025-01-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 131072, output: 8192 }, - }, - "llama-3.1-nemotron-ultra-253b": { - id: "llama-3.1-nemotron-ultra-253b", - name: "Llama 3.1 Nemotron Ultra 253B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-07", - last_updated: "2025-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 128000, output: 8192 }, - }, - "llama-4-maverick-17b-instruct": { - id: "llama-4-maverick-17b-instruct", - name: "Llama 4 Maverick 17B Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.24, output: 0.97 }, - limit: { context: 8192, output: 2048 }, - }, - "grok-4-0709": { - id: "grok-4-0709", - name: "Grok 4 (0709)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 256000, output: 256000 }, - }, - "qwen3-30b-a3b-fp8": { - id: "qwen3-30b-a3b-fp8", - name: "Qwen3 30B A3B FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "minimax-m2.1-lightning": { - id: "minimax-m2.1-lightning", - name: "MiniMax M2.1 Lightning", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.48 }, - limit: { context: 196608, output: 131072 }, - }, - "qwen3-max-2026-01-23": { - id: "qwen3-max-2026-01-23", - name: "Qwen3 Max (2026-01-23)", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.6 }, - limit: { context: 256000, output: 32800 }, - }, - "llama-3.2-3b-instruct": { - id: "llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-18", - last_updated: "2024-09-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.05 }, - limit: { context: 32768, output: 32000 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3 Coder Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4 }, - limit: { context: 262144, output: 65536 }, - }, - "gpt-4o-search-preview": { - id: "gpt-4o-search-preview", - name: "GPT-4o Search Preview", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 16384 }, - }, - custom: { - id: "custom", - name: "Custom Model", - family: "auto", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-01-01", - last_updated: "2024-01-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-vl-30b-a3b-instruct": { - id: "qwen3-vl-30b-a3b-instruct", - name: "Qwen3 VL 30B A3B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-10-02", - last_updated: "2025-10-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 131072, output: 8192 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 0.42, cache_read: 0.03 }, - limit: { context: 163840, output: 16384 }, - }, - "qwen3-235b-a22b-fp8": { - id: "qwen3-235b-a22b-fp8", - name: "Qwen3 235B A22B FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.5 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.5 }, - limit: { context: 131072, output: 32766 }, - }, - "kimi-k2": { - id: "kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.5 }, - limit: { context: 131072, output: 16384 }, - }, - "llama-3-8b-instruct": { - id: "llama-3-8b-instruct", - name: "Llama 3 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-04-03", - last_updated: "2025-04-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 8192, output: 8192 }, - }, - "qwen3-vl-235b-a22b-instruct": { - id: "qwen3-vl-235b-a22b-instruct", - name: "Qwen3 VL 235B A22B Instruct", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.75 }, - limit: { context: 131072, output: 32766 }, - }, - "qwen25-coder-7b": { - id: "qwen25-coder-7b", - name: "Qwen2.5 Coder 7B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-19", - last_updated: "2024-09-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.05 }, - limit: { context: 131072, output: 8192 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.22 }, - limit: { context: 128000, output: 2048 }, - status: "beta", - }, - "llama-3-70b-instruct": { - id: "llama-3-70b-instruct", - name: "Llama 3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.51, output: 0.74 }, - limit: { context: 8192, output: 8000 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek R1 (0528)", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 64000, output: 16384 }, - status: "beta", - }, - "glm-4.5-airx": { - id: "glm-4.5-airx", - name: "GLM-4.5 AirX", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.5, cache_read: 0.22 }, - limit: { context: 128000, output: 16384 }, - }, - "ministral-14b-2512": { - id: "ministral-14b-2512", - name: "Ministral 14B", - family: "mistral", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2025-12-02", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 262144, output: 8192 }, - }, - "llama-3.2-11b-instruct": { - id: "llama-3.2-11b-instruct", - name: "Llama 3.2 11B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.33 }, - limit: { context: 128000, output: 8192 }, - }, - "claude-3-opus": { - id: "claude-3-opus", - name: "Claude 3 Opus", - family: "claude", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5 }, - limit: { context: 200000, output: 4096 }, - }, - "minimax-m2.7": { - id: "minimax-m2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "grok-4-20-beta-0309-non-reasoning": { - id: "grok-4-20-beta-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "qwen3-coder-plus": { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 5 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-k2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "mistral-large-2512": { - id: "mistral-large-2512", - name: "Mistral Large 3", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "minimax-m2.7-highspeed": { - id: "minimax-m2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "gemma-3n-e4b-it": { - id: "gemma-3n-e4b-it", - name: "Gemma 3n 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "gpt-5.2-pro": { - id: "gpt-5.2-pro", - name: "GPT-5.2 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 21, output: 168 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "qwq-plus": { - id: "qwq-plus", - name: "QwQ Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 2.4 }, - limit: { context: 131072, output: 8192 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen-vl-plus": { - id: "qwen-vl-plus", - name: "Qwen-VL Plus", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-08-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.21, output: 0.63 }, - limit: { context: 131072, output: 8192 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "devstral-2512": { - id: "devstral-2512", - name: "Devstral 2", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen3-32b": { - id: "qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.7, output: 2.8, reasoning: 8.4 }, - limit: { context: 131072, output: 16384 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen35-397b-a17b": { - id: "qwen35-397b-a17b", - name: "Qwen3.5 397B-A17B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-02-15", - last_updated: "2026-02-15", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-max": { - id: "qwen-max", - name: "Qwen Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-03", - last_updated: "2025-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.6, output: 6.4 }, - limit: { context: 32768, output: 8192 }, - }, - "gpt-5.3-chat-latest": { - id: "gpt-5.3-chat-latest", - name: "GPT-5.3 Chat (latest)", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen-plus": { - id: "qwen-plus", - name: "Qwen Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-01-25", - last_updated: "2025-09-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.2, reasoning: 4 }, - limit: { context: 1000000, output: 32768 }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 12.5, output: 75, cache_read: 1.25 }, - provider: { body: { service_tier: "priority" } }, - }, - }, - }, - }, - "qwen3.6-35b-a3b": { - id: "qwen3.6-35b-a3b", - name: "Qwen3.6 35B-A3B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-17", - last_updated: "2026-04-17", - modalities: { input: ["text", "image", "video", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.248, output: 1.485 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen-omni-turbo": { - id: "qwen-omni-turbo", - name: "Qwen-Omni Turbo", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-01-19", - last_updated: "2025-03-26", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.07, output: 0.27, input_audio: 4.44, output_audio: 8.89 }, - limit: { context: 32768, output: 2048 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 131072 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "minimax-m2": { - id: "minimax-m2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 128000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen-flash": { - id: "qwen-flash", - name: "Qwen Flash", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4 }, - limit: { context: 1000000, output: 32768 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1048576, output: 131072 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "sonar-pro": { - id: "sonar-pro", - name: "Sonar Pro", - family: "sonar-pro", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15 }, - limit: { context: 200000, output: 8192 }, - }, - "pixtral-large-latest": { - id: "pixtral-large-latest", - name: "Pixtral Large (latest)", - family: "pixtral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2024-11-04", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-4-20-beta-0309-reasoning": { - id: "grok-4-20-beta-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-09", - last_updated: "2026-03-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6, cache_read: 0.2, context_over_200k: { input: 4, output: 12, cache_read: 0.4 } }, - limit: { context: 2000000, output: 30000 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3.6-plus": { - id: "qwen3.6-plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.276, output: 1.651, cache_read: 0.028, cache_write: 0.344 }, - limit: { context: 1000000, output: 65536 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "qwen3-max": { - id: "qwen3-max", - name: "Qwen3 Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.2, output: 6 }, - limit: { context: 262144, output: 65536 }, - }, - "minimax-m2.1": { - id: "minimax-m2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 24, cache_read: 1.3, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "mistral-large-latest": { - id: "mistral-large-latest", - name: "Mistral Large (latest)", - family: "mistral-large", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2024-11-01", - last_updated: "2025-12-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 262144, output: 262144 }, - }, - "mistral-small-2506": { - id: "mistral-small-2506", - name: "Mistral Small 3.2", - family: "mistral-small", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-06-20", - last_updated: "2025-06-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5.2-chat-latest": { - id: "gpt-5.2-chat-latest", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "gemma-3n-e2b-it": { - id: "gemma-3n-e2b-it", - name: "Gemma 3n 2B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex mini", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-4-fast": { - id: "grok-4-fast", - name: "Grok 4 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-05-07", - last_updated: "2026-05-07", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "qwen3-next-80b-a3b-thinking": { - id: "qwen3-next-80b-a3b-thinking", - name: "Qwen3-Next 80B-A3B (Thinking)", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 6 }, - limit: { context: 131072, output: 32768 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemma-3-4b-it": { - id: "gemma-3-4b-it", - name: "Gemma 3 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.03, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-3.5-turbo": { - id: "gpt-3.5-turbo", - name: "GPT-3.5-turbo", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2021-09-01", - release_date: "2023-03-01", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5, cache_read: 1.25 }, - limit: { context: 16385, output: 4096 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "qwen-vl-max": { - id: "qwen-vl-max", - name: "Qwen-VL Max", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-04-08", - last_updated: "2025-08-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.2 }, - limit: { context: 131072, output: 8192 }, - }, - sonar: { - id: "sonar", - name: "Sonar", - family: "sonar", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 1 }, - limit: { context: 128000, output: 4096 }, - }, - "qwen3-coder-flash": { - id: "qwen3-coder-flash", - name: "Qwen3 Coder Flash", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 1.5 }, - limit: { context: 1000000, output: 65536 }, - }, - "grok-4-3": { - id: "grok-4-3", - name: "Grok 4.3", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-05-01", - last_updated: "2026-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 2.5, - cache_read: 0.2, - context_over_200k: { input: 2.5, output: 5, cache_read: 0.4 }, - }, - limit: { context: 1000000, output: 30000 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "deepseek-v4-flash": { - id: "deepseek-v4-flash", - name: "DeepSeek V4 Flash", - family: "deepseek-flash", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.28, cache_read: 0.028 }, - limit: { context: 1000000, output: 384000 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "qwen3-next-80b-a3b-instruct": { - id: "qwen3-next-80b-a3b-instruct", - name: "Qwen3-Next 80B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09", - last_updated: "2025-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2 }, - limit: { context: 131072, output: 32768 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 60 }, - limit: { context: 8192, output: 8192 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 15, cache_read: 0.25 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "qwen3-vl-plus": { - id: "qwen3-vl-plus", - name: "Qwen3-VL Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-23", - last_updated: "2025-09-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.6, reasoning: 4.8 }, - limit: { context: 262144, output: 32768 }, - }, - "grok-4-1-fast": { - id: "grok-4-1-fast", - name: "Grok 4.1 Fast", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-coder-480b-a35b-instruct": { - id: "qwen3-coder-480b-a35b-instruct", - name: "Qwen3-Coder 480B-A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.5, output: 7.5 }, - limit: { context: 262144, output: 65536 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "deepseek-v4-pro": { - id: "deepseek-v4-pro", - name: "DeepSeek V4 Pro", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-05", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.74, output: 3.48, cache_read: 0.145 }, - limit: { context: 1000000, output: 384000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "qwen3-coder-30b-a3b-instruct": { - id: "qwen3-coder-30b-a3b-instruct", - name: "Qwen3-Coder 30B-A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.45, output: 2.25 }, - limit: { context: 262144, output: 65536 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, input: 272000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "minimax-m2.5-highspeed": { - id: "minimax-m2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen-turbo": { - id: "qwen-turbo", - name: "Qwen Turbo", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-11-01", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.2, reasoning: 0.5 }, - limit: { context: 1000000, output: 16384 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen3.6-max-preview": { - id: "qwen3.6-max-preview", - name: "Qwen3.6 Max Preview", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.3, output: 7.8, cache_read: 0.13, cache_write: 1.625 }, - limit: { context: 262144, output: 65536 }, - }, - "gpt-5-chat-latest": { - id: "gpt-5-chat-latest", - name: "GPT-5 Chat (latest)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "qwen2-5-vl-72b-instruct": { - id: "qwen2-5-vl-72b-instruct", - name: "Qwen2.5-VL 72B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.8, output: 8.4 }, - limit: { context: 131072, output: 8192 }, - }, - "gpt-5.5-pro": { - id: "gpt-5.5-pro", - name: "GPT-5.5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-23", - last_updated: "2026-04-23", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "devstral-small-2507": { - id: "devstral-small-2507", - name: "Devstral Small", - family: "devstral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-07-10", - last_updated: "2025-07-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 128000 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - "sonar-reasoning-pro": { - id: "sonar-reasoning-pro", - name: "Sonar Reasoning Pro", - family: "sonar-reasoning", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-09-01", - release_date: "2024-01-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8 }, - limit: { context: 128000, output: 4096 }, - }, - }, - }, - "google-vertex": { - id: "google-vertex", - env: ["GOOGLE_VERTEX_PROJECT", "GOOGLE_VERTEX_LOCATION", "GOOGLE_APPLICATION_CREDENTIALS"], - npm: "@ai-sdk/google-vertex", - name: "Vertex", - doc: "https://cloud.google.com/vertex-ai/generative-ai/docs/models", - models: { - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 65536, output: 65536 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, cache_write: 0.383 }, - limit: { context: 1048576, output: 65536 }, - }, - "zai-org/glm-5-maas": { - id: "zai-org/glm-5-maas", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.1 }, - limit: { context: 202752, output: 131072 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "zai-org/glm-4.7-maas": { - id: "zai-org/glm-4.7-maas", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-06", - last_updated: "2026-01-06", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2 }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "deepseek-ai/deepseek-v3.2-maas": { - id: "deepseek-ai/deepseek-v3.2-maas", - name: "DeepSeek V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-12-17", - last_updated: "2026-04-04", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68, cache_read: 0.056 }, - limit: { context: 163840, output: 65536 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "deepseek-ai/deepseek-v3.1-maas": { - id: "deepseek-ai/deepseek-v3.1-maas", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text", "pdf"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.7 }, - limit: { context: 163840, output: 32768 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "openai/gpt-oss-120b-maas": { - id: "openai/gpt-oss-120b-maas", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-oss-20b-maas": { - id: "openai/gpt-oss-20b-maas", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.25 }, - limit: { context: 131072, output: 32768 }, - }, - "meta/llama-3.3-70b-instruct-maas": { - id: "meta/llama-3.3-70b-instruct-maas", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.72, output: 0.72 }, - limit: { context: 128000, output: 8192 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "meta/llama-4-maverick-17b-128e-instruct-maas": { - id: "meta/llama-4-maverick-17b-128e-instruct-maas", - name: "Llama 4 Maverick 17B 128E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-29", - last_updated: "2025-04-29", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.15 }, - limit: { context: 524288, output: 8192 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "qwen/qwen3-235b-a22b-instruct-2507-maas": { - id: "qwen/qwen3-235b-a22b-instruct-2507-maas", - name: "Qwen3 235B A22B Instruct", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-13", - last_updated: "2025-08-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.22, output: 0.88 }, - limit: { context: 262144, output: 16384 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "moonshotai/kimi-k2-thinking-maas": { - id: "moonshotai/kimi-k2-thinking-maas", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}/endpoints/openapi", - }, - }, - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-haiku-4-5@20251001": { - id: "claude-haiku-4-5@20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-sonnet-4-6@default": { - id: "claude-sonnet-4-6@default", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 3, - output: 15, - cache_read: 0.3, - cache_write: 3.75, - context_over_200k: { input: 6, output: 22.5, cache_read: 0.6, cache_write: 7.5 }, - }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-5-haiku@20241022": { - id: "claude-3-5-haiku@20241022", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-5-sonnet@20241022": { - id: "claude-3-5-sonnet@20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-opus-4-1@20250805": { - id: "claude-opus-4-1@20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-embedding-001": { - id: "gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-05", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 2048, output: 3072 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-sonnet-4@20250514": { - id: "claude-sonnet-4@20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-05-07", - last_updated: "2026-05-07", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-3-7-sonnet@20250219": { - id: "claude-3-7-sonnet@20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "claude-opus-4@20250514": { - id: "claude-opus-4@20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "claude-opus-4-5@20251101": { - id: "claude-opus-4-5@20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-sonnet-4-5@20250929": { - id: "claude-sonnet-4-5@20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-opus-4-6@default": { - id: "claude-opus-4-6@default", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - "claude-opus-4-7@default": { - id: "claude-opus-4-7@default", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 1000000, output: 128000 }, - provider: { npm: "@ai-sdk/google-vertex/anthropic" }, - }, - }, - }, - "cloudflare-workers-ai": { - id: "cloudflare-workers-ai", - env: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1", - name: "Cloudflare Workers AI", - doc: "https://developers.cloudflare.com/workers-ai/models/", - models: { - "@cf/zai-org/glm-4.7-flash": { - id: "@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.06, output: 0.4 }, - limit: { context: 131072, output: 131072 }, - }, - "@cf/nvidia/nemotron-3-120b-a12b": { - id: "@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - family: "nemotron", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-11", - last_updated: "2026-03-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 256000, output: 256000 }, - }, - "@cf/openai/gpt-oss-20b": { - id: "@cf/openai/gpt-oss-20b", - name: "GPT OSS 20B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.3 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/openai/gpt-oss-120b": { - id: "@cf/openai/gpt-oss-120b", - name: "GPT OSS 120B", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 0.75 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/meta/llama-4-scout-17b-16e-instruct": { - id: "@cf/meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.27, output: 0.85 }, - limit: { context: 128000, output: 16384 }, - }, - "@cf/google/gemma-4-26b-a4b-it": { - id: "@cf/google/gemma-4-26b-a4b-it", - name: "Gemma 4 26B A4B IT", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-15", - last_updated: "2025-12-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 256000, output: 16384 }, - }, - "@cf/moonshotai/kimi-k2.5": { - id: "@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 256000, output: 256000 }, - }, - "@cf/moonshotai/kimi-k2.6": { - id: "@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 256000, output: 256000 }, - }, - }, - }, - groq: { - id: "groq", - env: ["GROQ_API_KEY"], - npm: "@ai-sdk/groq", - name: "Groq", - doc: "https://console.groq.com/docs/models", - models: { - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - family: "gemma", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-06-27", - last_updated: "2024-06-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-02-06", - last_updated: "2025-02-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.79, output: 0.79 }, - limit: { context: 32768, output: 32768 }, - status: "deprecated", - }, - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.75, output: 0.99 }, - limit: { context: 131072, output: 8192 }, - status: "deprecated", - }, - "llama-guard-3-8b": { - id: "llama-guard-3-8b", - name: "Llama Guard 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "llama-3.3-70b-versatile": { - id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 131072, output: 32768 }, - }, - "allam-2-7b": { - id: "allam-2-7b", - name: "ALLaM-2-7b", - family: "allam", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-09", - release_date: "2024-09", - last_updated: "2024-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 4096 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper Large V3", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2025-09-05", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 448, output: 448 }, - }, - "llama-3.1-8b-instant": { - id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 131072, output: 131072 }, - }, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.59, output: 0.79 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-27", - last_updated: "2024-11-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.39 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "whisper-large-v3-turbo": { - id: "whisper-large-v3-turbo", - name: "Whisper Large v3 Turbo", - family: "whisper", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 448, output: 448 }, - }, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.08 }, - limit: { context: 8192, output: 8192 }, - status: "deprecated", - }, - "canopylabs/orpheus-arabic-saudi": { - id: "canopylabs/orpheus-arabic-saudi", - name: "Orpheus Arabic Saudi", - family: "canopylabs", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-12-16", - release_date: "2025-12-16", - last_updated: "2025-12-16", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 40, output: 0 }, - limit: { context: 4000, output: 50000 }, - }, - "canopylabs/orpheus-v1-english": { - id: "canopylabs/orpheus-v1-english", - name: "Orpheus V1 English", - family: "canopylabs", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2025-12-19", - release_date: "2025-12-19", - last_updated: "2025-12-19", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 4000, output: 50000 }, - }, - "meta-llama/llama-4-scout-17b-16e-instruct": { - id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11, output: 0.34 }, - limit: { context: 131072, output: 8192 }, - }, - "meta-llama/llama-prompt-guard-2-22m": { - id: "meta-llama/llama-prompt-guard-2-22m", - name: "Llama Prompt Guard 2 22M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.03 }, - limit: { context: 512, output: 512 }, - }, - "meta-llama/llama-guard-4-12b": { - id: "meta-llama/llama-guard-4-12b", - name: "Llama Guard 4 12B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.2 }, - limit: { context: 131072, output: 1024 }, - status: "deprecated", - }, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 131072, output: 8192 }, - status: "deprecated", - }, - "meta-llama/llama-prompt-guard-2-86m": { - id: "meta-llama/llama-prompt-guard-2-86m", - name: "Llama Prompt Guard 2 86M", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2024-10-01", - last_updated: "2024-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 512, output: 512 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-safeguard-20b": { - id: "openai/gpt-oss-safeguard-20b", - name: "Safety GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-03-05", - last_updated: "2025-03-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3, cache_read: 0.037 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 65536 }, - }, - "qwen/qwen3-32b": { - id: "qwen/qwen3-32b", - name: "Qwen3 32B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11-08", - release_date: "2024-12-23", - last_updated: "2024-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.29, output: 0.59 }, - limit: { context: 131072, output: 40960 }, - }, - "groq/compound": { - id: "groq/compound", - name: "Compound", - family: "groq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09-04", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - family: "groq", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09-04", - release_date: "2025-09-04", - last_updated: "2025-09-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 131072, output: 16384 }, - status: "deprecated", - }, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3 }, - limit: { context: 262144, output: 16384 }, - }, - }, - }, - azure: { - id: "azure", - env: ["AZURE_RESOURCE_NAME", "AZURE_API_KEY"], - npm: "@ai-sdk/azure", - name: "Azure", - doc: "https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models", - models: { - "mistral-nemo": { - id: "mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.2-chat": { - id: "gpt-5.2-chat", - name: "GPT-5.2 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "codex-mini": { - id: "codex-mini", - name: "Codex Mini", - family: "gpt-codex-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-04", - release_date: "2025-05-16", - last_updated: "2025-05-16", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 6, cache_read: 0.375 }, - limit: { context: 200000, output: 100000 }, - }, - "phi-4-multimodal": { - id: "phi-4-multimodal", - name: "Phi-4-multimodal", - family: "phi", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0.08, output: 0.32, input_audio: 4 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-3.5-mini-instruct": { - id: "phi-3.5-mini-instruct", - name: "Phi-3.5-mini-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "llama-4-scout-17b-16e-instruct": { - id: "llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.78 }, - limit: { context: 128000, output: 8192 }, - }, - "grok-4-1-fast-reasoning": { - id: "grok-4-1-fast-reasoning", - name: "Grok 4.1 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 128000, input: 128000, output: 8192 }, - status: "beta", - }, - "phi-3-medium-4k-instruct": { - id: "phi-3-medium-4k-instruct", - name: "Phi-3-medium-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 4096, output: 1024 }, - }, - "ministral-3b": { - id: "ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.04, output: 0.04 }, - limit: { context: 128000, output: 8192 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-02-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "meta-llama-3.1-8b-instruct": { - id: "meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 128000, output: 32768 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-06", - last_updated: "2026-02-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.71, output: 0.71 }, - limit: { context: 128000, output: 32768 }, - }, - "deepseek-v3-0324": { - id: "deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.14, output: 4.56 }, - limit: { context: 131072, output: 131072 }, - }, - "gpt-5-chat": { - id: "gpt-5-chat", - name: "GPT-5 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-10-24", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 128000, output: 16384 }, - }, - "phi-3.5-moe-instruct": { - id: "phi-3.5-moe-instruct", - name: "Phi-3.5-MoE-instruct", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.64 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.3-chat": { - id: "gpt-5.3-chat", - name: "GPT-5.3 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 128000, output: 16384 }, - }, - "o1-mini": { - id: "o1-mini", - name: "o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 128000, output: 65536 }, - }, - "text-embedding-3-large": { - id: "text-embedding-3-large", - name: "text-embedding-3-large", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.13, output: 0 }, - limit: { context: 8191, output: 3072 }, - }, - "phi-3-mini-128k-instruct": { - id: "phi-3-mini-128k-instruct", - name: "Phi-3-mini-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 128000, output: 4096 }, - }, - "phi-4-reasoning": { - id: "phi-4-reasoning", - name: "Phi-4-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.03 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-5-nano": { - id: "gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.01 }, - limit: { context: 272000, output: 128000 }, - }, - "meta-llama-3-70b-instruct": { - id: "meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 8192, output: 2048 }, - }, - "phi-3-small-8k-instruct": { - id: "phi-3-small-8k-instruct", - name: "Phi-3-small-instruct (8k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 8192, output: 2048 }, - }, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-24", - last_updated: "2026-02-24", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "text-embedding-ada-002": { - id: "text-embedding-ada-002", - name: "text-embedding-ada-002", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2022-12-15", - last_updated: "2022-12-15", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0 }, - limit: { context: 8192, output: 1536 }, - }, - "llama-3.2-90b-vision-instruct": { - id: "llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.04, output: 2.04 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek-r1": { - id: "deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "grok-4-1-fast-non-reasoning": { - id: "grok-4-1-fast-non-reasoning", - name: "Grok 4.1 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-06-27", - last_updated: "2025-06-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 128000, input: 128000, output: 8192 }, - status: "beta", - }, - "deepseek-v3.2-speciale": { - id: "deepseek-v3.2-speciale", - name: "DeepSeek-V3.2-Speciale", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "mistral-large-2411": { - id: "mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 128000, output: 32768 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "cohere-command-a": { - id: "cohere-command-a", - name: "Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 256000, output: 8000 }, - }, - "llama-3.2-11b-vision-instruct": { - id: "llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.37, output: 0.37 }, - limit: { context: 128000, output: 8192 }, - }, - "meta-llama-3.1-405b-instruct": { - id: "meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 5.33, output: 16 }, - limit: { context: 128000, output: 32768 }, - }, - "gpt-5.1-chat": { - id: "gpt-5.1-chat", - name: "GPT-5.1 Chat", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-4-turbo-vision": { - id: "gpt-4-turbo-vision", - name: "GPT-4 Turbo Vision", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-14", - last_updated: "2026-01-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.75, output: 14, cache_read: 0.175 }, - limit: { context: 400000, output: 128000 }, - }, - "cohere-embed-v-4-0": { - id: "cohere-embed-v-4-0", - name: "Embed v4", - family: "cohere-embed", - attachment: true, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2025-04-15", - last_updated: "2025-04-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0 }, - limit: { context: 128000, output: 1536 }, - }, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "gpt-3.5-turbo-0125": { - id: "gpt-3.5-turbo-0125", - name: "GPT-3.5 Turbo 0125", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 1.5 }, - limit: { context: 16384, output: 16384 }, - }, - "o1-preview": { - id: "o1-preview", - name: "o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 16.5, output: 66, cache_read: 8.25 }, - limit: { context: 128000, output: 32768 }, - }, - "cohere-embed-v3-multilingual": { - id: "cohere-embed-v3-multilingual", - name: "Embed v3 Multilingual", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "grok-4-20-non-reasoning": { - id: "grok-4-20-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 272000, output: 128000 }, - }, - "grok-4-fast-reasoning": { - id: "grok-4-fast-reasoning", - name: "Grok 4 Fast (Reasoning)", - family: "grok", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - o1: { - id: "o1", - name: "o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2023-09", - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 60, cache_read: 7.5 }, - limit: { context: 200000, output: 100000 }, - }, - "mistral-small-2503": { - id: "mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.3 }, - limit: { context: 128000, output: 32768 }, - }, - "model-router": { - id: "model-router", - name: "Model Router", - family: "model-router", - attachment: true, - reasoning: false, - tool_call: true, - release_date: "2025-05-19", - last_updated: "2025-11-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-3.5-turbo-1106": { - id: "gpt-3.5-turbo-1106", - name: "GPT-3.5 Turbo 1106", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-11-06", - last_updated: "2023-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 2 }, - limit: { context: 16384, output: 16384 }, - }, - "text-embedding-3-small": { - id: "text-embedding-3-small", - name: "text-embedding-3-small", - family: "text-embedding", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2024-01-25", - last_updated: "2024-01-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.02, output: 0 }, - limit: { context: 8191, output: 1536 }, - }, - "deepseek-v3.1": { - id: "deepseek-v3.1", - name: "DeepSeek-V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.56, output: 1.68 }, - limit: { context: 131072, output: 131072 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-08-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-3-mini-4k-instruct": { - id: "phi-3-mini-4k-instruct", - name: "Phi-3-mini-instruct (4k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.13, output: 0.52 }, - limit: { context: 4096, output: 1024 }, - }, - "meta-llama-3.1-70b-instruct": { - id: "meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.68, output: 3.54 }, - limit: { context: 128000, output: 32768 }, - }, - "phi-4-mini-reasoning": { - id: "phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 8192, output: 8192 }, - }, - "meta-llama-3-8b-instruct": { - id: "meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.61 }, - limit: { context: 8192, output: 2048 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4 }, - limit: { context: 262144, output: 262144 }, - provider: { - npm: "@ai-sdk/openai-compatible", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/models", - shape: "completions", - }, - }, - "gpt-5-codex": { - id: "gpt-5-codex", - name: "GPT-5-Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 400000, output: 128000 }, - }, - "phi-4-mini": { - id: "phi-4-mini", - name: "Phi-4-mini", - family: "phi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 128000, output: 4096 }, - }, - "grok-4-20-reasoning": { - id: "grok-4-20-reasoning", - name: "Grok 4.20 (Reasoning)", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-09", - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 6 }, - limit: { context: 262000, output: 8192 }, - status: "beta", - }, - "gpt-3.5-turbo-0301": { - id: "gpt-3.5-turbo-0301", - name: "GPT-3.5 Turbo 0301", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-03-01", - last_updated: "2023-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 5, - output: 25, - cache_read: 0.5, - cache_write: 6.25, - context_over_200k: { input: 10, output: 37.5, cache_read: 1, cache_write: 12.5 }, - }, - limit: { context: 200000, output: 128000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-3-small-128k-instruct": { - id: "phi-3-small-128k-instruct", - name: "Phi-3-small-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4096 }, - }, - "deepseek-v3.2": { - id: "deepseek-v3.2", - name: "DeepSeek-V3.2", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.58, output: 1.68 }, - limit: { context: 128000, output: 128000 }, - }, - "phi-3-medium-128k-instruct": { - id: "phi-3-medium-128k-instruct", - name: "Phi-3-medium-instruct (128k)", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.17, output: 0.68 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-3.5-turbo-0613": { - id: "gpt-3.5-turbo-0613", - name: "GPT-3.5 Turbo 0613", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-06-13", - last_updated: "2023-06-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 4 }, - limit: { context: 16384, output: 16384 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "phi-4": { - id: "phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-5": { - id: "gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.13 }, - limit: { context: 272000, output: 128000 }, - }, - "gpt-4-32k": { - id: "gpt-4-32k", - name: "GPT-4 32K", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-11", - release_date: "2023-03-14", - last_updated: "2023-03-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 60, output: 120 }, - limit: { context: 32768, output: 32768 }, - }, - "cohere-embed-v3-english": { - id: "cohere-embed-v3-english", - name: "Embed v3 English", - family: "cohere-embed", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2023-11-07", - last_updated: "2023-11-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0 }, - limit: { context: 512, output: 1024 }, - }, - "phi-4-reasoning-plus": { - id: "phi-4-reasoning-plus", - name: "Phi-4-reasoning-plus", - family: "phi", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.125, output: 0.5 }, - limit: { context: 32000, output: 4096 }, - }, - "mistral-medium-2505": { - id: "mistral-medium-2505", - name: "Mistral Medium 3", - family: "mistral-medium", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-05", - release_date: "2025-05-07", - last_updated: "2025-05-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2 }, - limit: { context: 128000, output: 128000 }, - }, - "gpt-3.5-turbo-instruct": { - id: "gpt-3.5-turbo-instruct", - name: "GPT-3.5 Turbo Instruct", - family: "gpt", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2021-08", - release_date: "2023-09-21", - last_updated: "2023-09-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.5, output: 2 }, - limit: { context: 4096, output: 4096 }, - }, - "deepseek-r1-0528": { - id: "deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 163840, output: 163840 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-12-02", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-14", - last_updated: "2025-11-14", - modalities: { input: ["text", "image", "audio"], output: ["text", "image", "audio"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "codestral-2501": { - id: "codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 256000, output: 256000 }, - }, - "llama-4-maverick-17b-128e-instruct-fp8": { - id: "llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-04-05", - last_updated: "2025-04-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.25, output: 1 }, - limit: { context: 128000, output: 8192 }, - }, - "mai-ds-r1": { - id: "mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.35, output: 5.4 }, - limit: { context: 128000, output: 8192 }, - }, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-11-13", - last_updated: "2025-11-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - provider: { - npm: "@ai-sdk/anthropic", - api: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1", - }, - }, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-12-01", - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 30, cache_read: 0.5, context_over_200k: { input: 10, output: 45, cache_read: 1 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2023-11-06", - last_updated: "2024-04-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 10, output: 30 }, - limit: { context: 128000, output: 4096 }, - }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.08 }, - limit: { context: 128000, output: 16384 }, - }, - "gpt-5.4-mini": { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 4.5, cache_read: 0.075 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "cohere-command-r-08-2024": { - id: "cohere-command-r-08-2024", - name: "Command R", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 128000, output: 4000 }, - }, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - family: "o-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.28 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5.4-nano": { - id: "gpt-5.4-nano", - name: "GPT-5.4 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.25, cache_read: 0.02 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2025-08-28", - last_updated: "2025-08-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 1.5, cache_read: 0.02 }, - limit: { context: 256000, output: 10000 }, - }, - "gpt-5.4-pro": { - id: "gpt-5.4-pro", - name: "GPT-5.4 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 30, output: 180, context_over_200k: { input: 60, output: 270 } }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2024-12-20", - last_updated: "2025-01-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1.1, output: 4.4, cache_read: 0.55 }, - limit: { context: 200000, output: 100000 }, - }, - "grok-4": { - id: "grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, reasoning: 15, cache_read: 0.75 }, - limit: { context: 256000, output: 64000 }, - }, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 2.5, - output: 15, - cache_read: 0.25, - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, - }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "gpt-4.1-nano": { - id: "gpt-4.1-nano", - name: "GPT-4.1 nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.03 }, - limit: { context: 1047576, output: 32768 }, - }, - "grok-3-mini": { - id: "grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 0.5, reasoning: 0.5, cache_read: 0.075 }, - limit: { context: 131072, output: 8192 }, - }, - o3: { - id: "o3", - name: "o3", - family: "o", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2024-05", - release_date: "2025-04-16", - last_updated: "2025-04-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 200000, output: 100000 }, - }, - "gpt-5-pro": { - id: "gpt-5-pro", - name: "GPT-5 Pro", - family: "gpt-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2025-10-06", - last_updated: "2025-10-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 120 }, - limit: { context: 400000, output: 272000 }, - }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-09", - release_date: "2024-05-13", - last_updated: "2024-08-06", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2.5, output: 10, cache_read: 1.25 }, - limit: { context: 128000, output: 16384 }, - }, - "cohere-command-r-plus-08-2024": { - id: "cohere-command-r-plus-08-2024", - name: "Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-06-01", - release_date: "2024-08-30", - last_updated: "2024-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 10 }, - limit: { context: 128000, output: 4000 }, - }, - "gpt-4.1": { - id: "gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "gpt-4.1-mini": { - id: "gpt-4.1-mini", - name: "GPT-4.1 mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 1.6, cache_read: 0.1 }, - limit: { context: 1047576, output: 32768 }, - }, - "grok-3": { - id: "grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-11", - release_date: "2025-02-17", - last_updated: "2025-02-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75 }, - limit: { context: 131072, output: 8192 }, - }, - "grok-4-fast-non-reasoning": { - id: "grok-4-fast-non-reasoning", - name: "Grok 4 Fast (Non-Reasoning)", - family: "grok", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-19", - last_updated: "2025-09-19", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.2, output: 0.5, cache_read: 0.05 }, - limit: { context: 2000000, output: 30000 }, - }, - }, - }, - fastrouter: { - id: "fastrouter", - env: ["FASTROUTER_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://go.fastrouter.ai/api/v1", - name: "FastRouter", - doc: "https://fastrouter.ai/models", - models: { - "x-ai/grok-4": { - id: "x-ai/grok-4", - name: "Grok 4", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.75, cache_write: 15 }, - limit: { context: 256000, output: 64000 }, - }, - "deepseek-ai/deepseek-r1-distill-llama-70b": { - id: "deepseek-ai/deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-01-23", - last_updated: "2025-01-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.14 }, - limit: { context: 131072, output: 131072 }, - }, - "openai/gpt-5-mini": { - id: "openai/gpt-5-mini", - name: "GPT-5 Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2, cache_read: 0.025 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-5-nano": { - id: "openai/gpt-5-nano", - name: "GPT-5 Nano", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.4, cache_read: 0.005 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.2 }, - limit: { context: 131072, output: 65536 }, - }, - "openai/gpt-5": { - id: "openai/gpt-5", - name: "GPT-5", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-01", - release_date: "2025-08-07", - last_updated: "2025-08-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.125 }, - limit: { context: 400000, output: 128000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 32768 }, - }, - "z-ai/glm-5": { - id: "z-ai/glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 3.15 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwen3-coder": { - id: "qwen/qwen3-coder", - name: "Qwen3 Coder", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 66536 }, - }, - "google/gemini-2.5-pro": { - id: "google/gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "google/gemini-2.5-flash": { - id: "google/gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "moonshotai/kimi-k2": { - id: "moonshotai/kimi-k2", - name: "Kimi K2", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-11", - last_updated: "2025-07-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32768 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 8, cache_read: 0.5 }, - limit: { context: 1047576, output: 32768 }, - }, - "anthropic/claude-opus-4.1": { - id: "anthropic/claude-opus-4.1", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - stackit: { - id: "stackit", - env: ["STACKIT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1", - name: "STACKIT", - doc: "https://docs.stackit.cloud/products/data-and-ai/ai-model-serving/basics/available-shared-models", - models: { - "Qwen/Qwen3-VL-Embedding-8B": { - id: "Qwen/Qwen3-VL-Embedding-8B", - name: "Qwen3-VL Embedding 8B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.09 }, - limit: { context: 32000, output: 4096 }, - }, - "Qwen/Qwen3-VL-235B-A22B-Instruct-FP8": { - id: "Qwen/Qwen3-VL-235B-A22B-Instruct-FP8", - name: "Qwen3-VL 235B", - family: "qwen", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.64, output: 1.91 }, - limit: { context: 218000, output: 8192 }, - }, - "neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8": { - id: "neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8", - name: "Llama 3.1 8B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.16, output: 0.27 }, - limit: { context: 128000, output: 8192 }, - }, - "neuralmagic/Mistral-Nemo-Instruct-2407-FP8": { - id: "neuralmagic/Mistral-Nemo-Instruct-2407-FP8", - name: "Mistral Nemo", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-07-01", - last_updated: "2024-07-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS 120B", - family: "gpt", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 131000, output: 8192 }, - }, - "cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic": { - id: "cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2024-12-05", - last_updated: "2024-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 128000, output: 8192 }, - }, - "google/gemma-3-27b-it": { - id: "google/gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - release_date: "2025-05-17", - last_updated: "2025-05-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.49, output: 0.71 }, - limit: { context: 37000, output: 8192 }, - }, - "intfloat/e5-mistral-7b-instruct": { - id: "intfloat/e5-mistral-7b-instruct", - name: "E5 Mistral 7B", - family: "mistral", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: false, - release_date: "2023-12-11", - last_updated: "2023-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.02, output: 0.02 }, - limit: { context: 4096, output: 4096 }, - }, - }, - }, - "tencent-coding-plan": { - id: "tencent-coding-plan", - env: ["TENCENT_CODING_PLAN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.lkeap.cloud.tencent.com/coding/v3", - name: "Tencent Coding Plan (China)", - doc: "https://cloud.tencent.com/document/product/1772/128947", - models: { - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi-K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 202752, output: 16384 }, - }, - "hunyuan-turbos": { - id: "hunyuan-turbos", - name: "Hunyuan-TurboS", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-t1": { - id: "hunyuan-t1", - name: "Hunyuan-T1", - family: "hunyuan", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-2.0-instruct": { - id: "hunyuan-2.0-instruct", - name: "Tencent HY 2.0 Instruct", - family: "hunyuan", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 204800, output: 32768 }, - }, - "tc-code-latest": { - id: "tc-code-latest", - name: "Auto", - family: "auto", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - "hunyuan-2.0-thinking": { - id: "hunyuan-2.0-thinking", - name: "Tencent HY 2.0 Think", - family: "hunyuan", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-03-08", - last_updated: "2026-03-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 16384 }, - }, - }, - }, - "privatemode-ai": { - id: "privatemode-ai", - env: ["PRIVATEMODE_API_KEY", "PRIVATEMODE_ENDPOINT"], - npm: "@ai-sdk/openai-compatible", - api: "http://localhost:8080/v1", - name: "Privatemode AI", - doc: "https://docs.privatemode.ai/api/overview", - models: { - "gemma-3-27b": { - id: "gemma-3-27b", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-08", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "whisper-large-v3": { - id: "whisper-large-v3", - name: "Whisper large-v3", - family: "whisper", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2023-09", - release_date: "2023-09-01", - last_updated: "2023-09-01", - modalities: { input: ["audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 0, output: 4096 }, - }, - "qwen3-embedding-4b": { - id: "qwen3-embedding-4b", - name: "Qwen3-Embedding 4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - structured_output: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-06-06", - last_updated: "2025-06-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32000, output: 2560 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "gpt-oss-120b", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-04", - last_updated: "2025-08-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 128000 }, - }, - "qwen3-coder-30b-a3b": { - id: "qwen3-coder-30b-a3b", - name: "Qwen3-Coder 30B-A3B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04", - last_updated: "2025-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - }, - }, - google: { - id: "google", - env: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], - npm: "@ai-sdk/google", - name: "Google", - doc: "https://ai.google.dev/gemini-api/docs/models", - models: { - "gemini-flash-lite-latest": { - id: "gemini-flash-lite-latest", - name: "Gemini Flash-Lite Latest", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-05-06": { - id: "gemini-2.5-pro-preview-05-06", - name: "Gemini 2.5 Pro Preview 05-06", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-06", - last_updated: "2025-05-06", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-live-2.5-flash-preview-native-audio": { - id: "gemini-live-2.5-flash-preview-native-audio", - name: "Gemini Live 2.5 Flash Preview Native Audio", - family: "gemini-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-09-18", - modalities: { input: ["text", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.5, output: 2, input_audio: 3, output_audio: 12 }, - limit: { context: 131072, output: 65536 }, - }, - "gemini-3.1-pro-preview-customtools": { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-05-14", - last_updated: "2024-05-14", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3, cache_read: 0.01875 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-02-15", - last_updated: "2024-02-15", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 5, cache_read: 0.3125 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemma-3n-e4b-it": { - id: "gemma-3n-e4b-it", - name: "Gemma 3n 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "gemini-3.1-flash-lite-preview": { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-03-03", - last_updated: "2026-03-03", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-3.1-pro-preview": { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-19", - last_updated: "2026-02-19", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 8192 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 0.5, - output: 3, - cache_read: 0.05, - context_over_200k: { input: 0.5, output: 3, cache_read: 0.05 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-preview-tts": { - id: "gemini-2.5-flash-preview-tts", - name: "Gemini 2.5 Flash Preview TTS", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 0.5, output: 10 }, - limit: { context: 8000, output: 16000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-11-18", - last_updated: "2025-11-18", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 2, output: 12, cache_read: 0.2, context_over_200k: { input: 4, output: 18, cache_read: 0.4 } }, - limit: { context: 1000000, output: 64000 }, - }, - "gemini-2.5-flash-preview-05-20": { - id: "gemini-2.5-flash-preview-05-20", - name: "Gemini 2.5 Flash Preview 05-20", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-embedding-001": { - id: "gemini-embedding-001", - name: "Gemini Embedding 001", - family: "gemini", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-05", - release_date: "2025-05-20", - last_updated: "2025-05-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0 }, - limit: { context: 2048, output: 3072 }, - }, - "gemini-2.5-pro": { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { - input: 1.25, - output: 10, - cache_read: 0.125, - context_over_200k: { input: 2.5, output: 15, cache_read: 0.25 }, - }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-flash-latest": { - id: "gemini-flash-latest", - name: "Gemini Flash Latest", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-4-31b-it": { - id: "gemma-4-31b-it", - name: "Gemma 4 31B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 256000, output: 8192 }, - }, - "gemini-2.5-pro-preview-06-05": { - id: "gemini-2.5-pro-preview-06-05", - name: "Gemini 2.5 Pro Preview 06-05", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-05", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1.25, output: 10, cache_read: 0.31 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-image": { - id: "gemini-2.5-flash-image", - name: "Gemini 2.5 Flash Image", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 30, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "gemini-2.5-flash-lite-preview-06-17": { - id: "gemini-2.5-flash-lite-preview-06-17", - name: "Gemini 2.5 Flash Lite Preview 06-17", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025, input_audio: 0.3 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3-12b-it": { - id: "gemma-3-12b-it", - name: "Gemma 3 12B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-03-20", - last_updated: "2025-06-05", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.03, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3n-e2b-it": { - id: "gemma-3n-e2b-it", - name: "Gemma 3n 2B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-09", - last_updated: "2025-07-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2000 }, - }, - "gemini-3.1-flash-image-preview": { - id: "gemini-3.1-flash-image-preview", - name: "Gemini 3.1 Flash Image (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-01", - release_date: "2026-02-26", - last_updated: "2026-02-26", - modalities: { input: ["text", "image", "pdf"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.25, output: 60 }, - limit: { context: 131072, output: 32768 }, - }, - "gemini-3.1-flash-lite": { - id: "gemini-3.1-flash-lite", - name: "Gemini 3.1 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-05-07", - last_updated: "2026-05-07", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.5, cache_read: 0.025, cache_write: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3-4b-it": { - id: "gemma-3-4b-it", - name: "Gemma 3 4B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-13", - last_updated: "2025-03-13", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 32768, output: 8192 }, - }, - "gemini-2.5-flash-preview-04-17": { - id: "gemini-2.5-flash-preview-04-17", - name: "Gemini 2.5 Flash Preview 04-17", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-04-17", - last_updated: "2025-04-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.15, output: 0.6, cache_read: 0.0375 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-pro-preview-tts": { - id: "gemini-2.5-pro-preview-tts", - name: "Gemini 2.5 Pro Preview TTS", - family: "gemini-flash", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - knowledge: "2025-01", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: false, - cost: { input: 1, output: 20 }, - limit: { context: 8000, output: 16000 }, - }, - "gemini-2.5-flash-preview-09-2025": { - id: "gemini-2.5-flash-preview-09-2025", - name: "Gemini 2.5 Flash Preview 09-25", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-25", - last_updated: "2025-09-25", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.3, output: 2.5, cache_read: 0.075, input_audio: 1 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemma-3-27b-it": { - id: "gemma-3-27b-it", - name: "Gemma 3 27B", - family: "gemma", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-03-12", - last_updated: "2025-03-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 8192 }, - }, - "gemma-4-26b-a4b-it": { - id: "gemma-4-26b-a4b-it", - name: "Gemma 4 26B", - family: "gemma", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - limit: { context: 256000, output: 8192 }, - }, - "gemini-2.5-flash-lite": { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-06-17", - last_updated: "2025-06-17", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.1, output: 0.4, cache_read: 0.025 }, - limit: { context: 1048576, output: 65536 }, - }, - "gemini-2.5-flash-image-preview": { - id: "gemini-2.5-flash-image-preview", - name: "Gemini 2.5 Flash Image (Preview)", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2025-06", - release_date: "2025-08-26", - last_updated: "2025-08-26", - modalities: { input: ["text", "image"], output: ["text", "image"] }, - open_weights: false, - cost: { input: 0.3, output: 30, cache_read: 0.075 }, - limit: { context: 32768, output: 32768 }, - }, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B", - family: "gemini-flash", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2024-10-03", - last_updated: "2024-10-03", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.0375, output: 0.15, cache_read: 0.01 }, - limit: { context: 1000000, output: 8192 }, - }, - "gemini-live-2.5-flash": { - id: "gemini-live-2.5-flash", - name: "Gemini Live 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-09-01", - last_updated: "2025-09-01", - modalities: { input: ["text", "image", "audio", "video"], output: ["text", "audio"] }, - open_weights: false, - cost: { input: 0.5, output: 2, input_audio: 3, output_audio: 12 }, - limit: { context: 128000, output: 8000 }, - }, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - family: "gemini-flash-lite", - attachment: true, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2024-06", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.075, output: 0.3 }, - limit: { context: 1048576, output: 8192 }, - }, - }, - }, - drun: { - id: "drun", - env: ["DRUN_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://chat.d.run/v1", - name: "D.Run (China)", - doc: "https://www.d.run", - models: { - "public/deepseek-r1": { - id: "public/deepseek-r1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.55, output: 2.2 }, - limit: { context: 131072, output: 32000 }, - }, - "public/minimax-m25": { - id: "public/minimax-m25", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_details" }, - temperature: true, - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.29, output: 1.16 }, - limit: { context: 204800, output: 131072 }, - }, - "public/deepseek-v3": { - id: "public/deepseek-v3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2024-12-26", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.28, output: 1.1 }, - limit: { context: 131072, output: 8192 }, - }, - }, - }, - moonshotai: { - id: "moonshotai", - env: ["MOONSHOT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.moonshot.ai/v1", - name: "Moonshot AI", - doc: "https://platform.moonshot.ai/docs/api/chat", - models: { - "kimi-k2-0905-preview": { - id: "kimi-k2-0905-preview", - name: "Kimi K2 0905", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - family: "kimi-k2.5", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01", - release_date: "2026-01", - last_updated: "2026-01", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3, cache_read: 0.1 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-thinking-turbo": { - id: "kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.15, output: 8, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2.6": { - id: "kimi-k2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4, cache_read: 0.16 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-turbo-preview": { - id: "kimi-k2-turbo-preview", - name: "Kimi K2 Turbo", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-09-05", - last_updated: "2025-09-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.4, output: 10, cache_read: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "kimi-k2-0711-preview": { - id: "kimi-k2-0711-preview", - name: "Kimi K2 0711", - family: "kimi", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-07-14", - last_updated: "2025-07-14", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 131072, output: 16384 }, - }, - "kimi-k2-thinking": { - id: "kimi-k2-thinking", - name: "Kimi K2 Thinking", - family: "kimi-thinking", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-08", - release_date: "2025-11-06", - last_updated: "2025-11-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.5, cache_read: 0.15 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - berget: { - id: "berget", - env: ["BERGET_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.berget.ai/v1", - name: "Berget.AI", - doc: "https://api.berget.ai", - models: { - "zai-org/GLM-4.7": { - id: "zai-org/GLM-4.7", - name: "GLM 4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.77, output: 2.75 }, - limit: { context: 128000, output: 8192 }, - }, - "mistralai/Mistral-Small-3.2-24B-Instruct-2506": { - id: "mistralai/Mistral-Small-3.2-24B-Instruct-2506", - name: "Mistral Small 3.2 24B Instruct 2506", - family: "mistral-small", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-09", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.33, output: 0.33 }, - limit: { context: 32000, output: 8192 }, - }, - "mistralai/Mistral-Medium-3.5-128B": { - id: "mistralai/Mistral-Medium-3.5-128B", - name: "Mistral Medium 3.5 128B", - family: "mistral-medium", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2026-04", - release_date: "2026-04-29", - last_updated: "2026-04-29", - modalities: { input: ["image", "text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.65, output: 5.5 }, - limit: { context: 262144, output: 131072 }, - }, - "meta-llama/Llama-3.3-70B-Instruct": { - id: "meta-llama/Llama-3.3-70B-Instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2023-12", - release_date: "2025-04-27", - last_updated: "2025-04-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.99, output: 0.99 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT-OSS-120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.44, output: 0.99 }, - limit: { context: 128000, output: 8192 }, - }, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-12", - release_date: "2026-04-02", - last_updated: "2026-04-02", - modalities: { input: ["audio", "image", "text", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.275, output: 0.55 }, - limit: { context: 128000, output: 8192 }, - }, - }, - }, - "github-models": { - id: "github-models", - env: ["GITHUB_TOKEN"], - npm: "@ai-sdk/openai-compatible", - api: "https://models.github.ai/inference", - name: "GitHub Models", - doc: "https://docs.github.com/en/github-models", - models: { - "deepseek/deepseek-v3-0324": { - id: "deepseek/deepseek-v3-0324", - name: "DeepSeek-V3-0324", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-03-24", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "deepseek/deepseek-r1": { - id: "deepseek/deepseek-r1", - name: "DeepSeek-R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "deepseek/deepseek-r1-0528": { - id: "deepseek/deepseek-r1-0528", - name: "DeepSeek-R1-0528", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-05-28", - last_updated: "2025-05-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "ai21-labs/ai21-jamba-1.5-mini": { - id: "ai21-labs/ai21-jamba-1.5-mini", - name: "AI21 Jamba 1.5 Mini", - family: "jamba", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-29", - last_updated: "2024-08-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 4096 }, - }, - "ai21-labs/ai21-jamba-1.5-large": { - id: "ai21-labs/ai21-jamba-1.5-large", - name: "AI21 Jamba 1.5 Large", - family: "jamba", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-29", - last_updated: "2024-08-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 256000, output: 4096 }, - }, - "microsoft/phi-3.5-mini-instruct": { - id: "microsoft/phi-3.5-mini-instruct", - name: "Phi-3.5-mini instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-4k-instruct": { - id: "microsoft/phi-3-medium-4k-instruct", - name: "Phi-3-medium instruct (4k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 1024 }, - }, - "microsoft/phi-3.5-moe-instruct": { - id: "microsoft/phi-3.5-moe-instruct", - name: "Phi-3.5-MoE instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-mini-128k-instruct": { - id: "microsoft/phi-3-mini-128k-instruct", - name: "Phi-3-mini instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-mini-instruct": { - id: "microsoft/phi-4-mini-instruct", - name: "Phi-4-mini-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4-reasoning": { - id: "microsoft/phi-4-reasoning", - name: "Phi-4-Reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-8k-instruct": { - id: "microsoft/phi-3-small-8k-instruct", - name: "Phi-3-small instruct (8k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "microsoft/phi-3.5-vision-instruct": { - id: "microsoft/phi-3.5-vision-instruct", - name: "Phi-3.5-vision instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-08-20", - last_updated: "2024-08-20", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-mini-4k-instruct": { - id: "microsoft/phi-3-mini-4k-instruct", - name: "Phi-3-mini instruct (4k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 4096, output: 1024 }, - }, - "microsoft/phi-4-mini-reasoning": { - id: "microsoft/phi-4-mini-reasoning", - name: "Phi-4-mini-reasoning", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-small-128k-instruct": { - id: "microsoft/phi-3-small-128k-instruct", - name: "Phi-3-small instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-3-medium-128k-instruct": { - id: "microsoft/phi-3-medium-128k-instruct", - name: "Phi-3-medium instruct (128k)", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-04-23", - last_updated: "2024-04-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/phi-4": { - id: "microsoft/phi-4", - name: "Phi-4", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 16000, output: 4096 }, - }, - "microsoft/phi-4-multimodal-instruct": { - id: "microsoft/phi-4-multimodal-instruct", - name: "Phi-4-multimodal-instruct", - family: "phi", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-12-11", - last_updated: "2024-12-11", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "microsoft/mai-ds-r1": { - id: "microsoft/mai-ds-r1", - name: "MAI-DS-R1", - family: "mai", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-06", - release_date: "2025-01-20", - last_updated: "2025-01-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 65536, output: 8192 }, - }, - "cohere/cohere-command-r-08-2024": { - id: "cohere/cohere-command-r-08-2024", - name: "Cohere Command R 08-2024", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-a": { - id: "cohere/cohere-command-a", - name: "Cohere Command A", - family: "command-a", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r-plus": { - id: "cohere/cohere-command-r-plus", - name: "Cohere Command R+", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-04-04", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r": { - id: "cohere/cohere-command-r", - name: "Cohere Command R", - family: "command-r", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-03-11", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "cohere/cohere-command-r-plus-08-2024": { - id: "cohere/cohere-command-r-plus-08-2024", - name: "Cohere Command R+ 08-2024", - family: "command-r", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-08-01", - last_updated: "2024-08-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 4096 }, - }, - "xai/grok-3-mini": { - id: "xai/grok-3-mini", - name: "Grok 3 Mini", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-09", - last_updated: "2024-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "xai/grok-3": { - id: "xai/grok-3", - name: "Grok 3", - family: "grok", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2024-12-09", - last_updated: "2024-12-09", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "openai/o1-mini": { - id: "openai/o1-mini", - name: "OpenAI o1-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-12-17", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 65536 }, - }, - "openai/gpt-4o-mini": { - id: "openai/gpt-4o-mini", - name: "GPT-4o mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o4-mini": { - id: "openai/o4-mini", - name: "OpenAI o4-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o1-preview": { - id: "openai/o1-preview", - name: "OpenAI o1-preview", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-09-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "openai/o1": { - id: "openai/o1", - name: "OpenAI o1", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2023-10", - release_date: "2024-09-12", - last_updated: "2024-12-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/o3-mini": { - id: "openai/o3-mini", - name: "OpenAI o3-mini", - family: "o-mini", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4.1-nano": { - id: "openai/gpt-4.1-nano", - name: "GPT-4.1-nano", - family: "gpt-nano", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/o3": { - id: "openai/o3", - name: "OpenAI o3", - family: "o", - attachment: false, - reasoning: true, - tool_call: false, - temperature: false, - knowledge: "2024-04", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 200000, output: 100000 }, - }, - "openai/gpt-4o": { - id: "openai/gpt-4o", - name: "GPT-4o", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-10", - release_date: "2024-05-13", - last_updated: "2024-05-13", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1": { - id: "openai/gpt-4.1", - name: "GPT-4.1", - family: "gpt", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "openai/gpt-4.1-mini": { - id: "openai/gpt-4.1-mini", - name: "GPT-4.1-mini", - family: "gpt-mini", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04", - release_date: "2025-04-14", - last_updated: "2025-04-14", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 16384 }, - }, - "meta/llama-4-scout-17b-16e-instruct": { - id: "meta/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B 16E Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/meta-llama-3.1-8b-instruct": { - id: "meta/meta-llama-3.1-8b-instruct", - name: "Meta-Llama-3.1-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/llama-3.3-70b-instruct": { - id: "meta/llama-3.3-70b-instruct", - name: "Llama-3.3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3-70b-instruct": { - id: "meta/meta-llama-3-70b-instruct", - name: "Meta-Llama-3-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "meta/llama-3.2-90b-vision-instruct": { - id: "meta/llama-3.2-90b-vision-instruct", - name: "Llama-3.2-90B-Vision-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/llama-3.2-11b-vision-instruct": { - id: "meta/llama-3.2-11b-vision-instruct", - name: "Llama-3.2-11B-Vision-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-09-25", - last_updated: "2024-09-25", - modalities: { input: ["text", "image", "audio"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "meta/meta-llama-3.1-405b-instruct": { - id: "meta/meta-llama-3.1-405b-instruct", - name: "Meta-Llama-3.1-405B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3.1-70b-instruct": { - id: "meta/meta-llama-3.1-70b-instruct", - name: "Meta-Llama-3.1-70B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-07-23", - last_updated: "2024-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "meta/meta-llama-3-8b-instruct": { - id: "meta/meta-llama-3-8b-instruct", - name: "Meta-Llama-3-8B-Instruct", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-04-18", - last_updated: "2024-04-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "meta/llama-4-maverick-17b-128e-instruct-fp8": { - id: "meta/llama-4-maverick-17b-128e-instruct-fp8", - name: "Llama 4 Maverick 17B 128E Instruct FP8", - family: "llama", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-12", - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "core42/jais-30b-chat": { - id: "core42/jais-30b-chat", - name: "JAIS 30b Chat", - family: "jais", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2023-03", - release_date: "2023-08-30", - last_updated: "2023-08-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 2048 }, - }, - "mistral-ai/mistral-nemo": { - id: "mistral-ai/mistral-nemo", - name: "Mistral Nemo", - family: "mistral-nemo", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-07-18", - last_updated: "2024-07-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral-ai/ministral-3b": { - id: "mistral-ai/ministral-3b", - name: "Ministral 3B", - family: "ministral", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 8192 }, - }, - "mistral-ai/mistral-large-2411": { - id: "mistral-ai/mistral-large-2411", - name: "Mistral Large 24.11", - family: "mistral-large", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2024-11-01", - last_updated: "2024-11-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/mistral-small-2503": { - id: "mistral-ai/mistral-small-2503", - name: "Mistral Small 3.1", - family: "mistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-03-01", - last_updated: "2025-03-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/mistral-medium-2505": { - id: "mistral-ai/mistral-medium-2505", - name: "Mistral Medium 3 (25.05)", - family: "mistral-medium", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09", - release_date: "2025-05-01", - last_updated: "2025-05-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 128000, output: 32768 }, - }, - "mistral-ai/codestral-2501": { - id: "mistral-ai/codestral-2501", - name: "Codestral 25.01", - family: "codestral", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-03", - release_date: "2025-01-01", - last_updated: "2025-01-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 32000, output: 8192 }, - }, - }, - }, - neuralwatt: { - id: "neuralwatt", - env: ["NEURALWATT_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.neuralwatt.com/v1", - name: "Neuralwatt", - doc: "https://portal.neuralwatt.com/docs", - models: { - "glm-5-fast": { - id: "glm-5-fast", - name: "GLM 5 Fast", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3.6 }, - limit: { context: 200000, output: 200000 }, - }, - "kimi-k2.6-fast": { - id: "kimi-k2.6-fast", - name: "Kimi K2.6 Fast", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.69, output: 3.22 }, - limit: { context: 262144, output: 262144 }, - }, - "qwen3.5-397b-fast": { - id: "qwen3.5-397b-fast", - name: "Qwen3.5 397B Fast", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.69, output: 4.14 }, - limit: { context: 262144, output: 262144 }, - }, - "glm-5.1-fast": { - id: "glm-5.1-fast", - name: "GLM 5.1 Fast", - family: "glm", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3.6 }, - limit: { context: 200000, output: 200000 }, - }, - "qwen3.6-35b-fast": { - id: "qwen3.6-35b-fast", - name: "Qwen3.6 35B Fast", - family: "qwen3.6", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "kimi-k2.5-fast": { - id: "kimi-k2.5-fast", - name: "Kimi K2.5 Fast", - family: "kimi", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.52, output: 2.59 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3.5-397B-A17B-FP8": { - id: "Qwen/Qwen3.5-397B-A17B-FP8", - name: "Qwen3.5 397B A17B FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.69, output: 4.14 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3.6-35B-A3B": { - id: "Qwen/Qwen3.6-35B-A3B", - name: "Qwen3.6 35B A3B", - family: "qwen3.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.05, output: 0.1 }, - limit: { context: 131072, output: 131072 }, - }, - "zai-org/GLM-5.1-FP8": { - id: "zai-org/GLM-5.1-FP8", - name: "GLM 5.1 FP8", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.1, output: 3.6 }, - limit: { context: 200000, output: 200000 }, - }, - "mistralai/Devstral-Small-2-24B-Instruct-2512": { - id: "mistralai/Devstral-Small-2-24B-Instruct-2512", - name: "Devstral Small 2 24B Instruct 2512", - family: "devstral", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-12-09", - last_updated: "2025-12-09", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.35 }, - limit: { context: 262144, output: 262144 }, - }, - "openai/gpt-oss-20b": { - id: "openai/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.03, output: 0.16 }, - limit: { context: 16384, output: 16384 }, - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.69, output: 3.22 }, - limit: { context: 262144, output: 262144 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.52, output: 2.59 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.35, output: 1.38 }, - limit: { context: 196608, output: 196608 }, - }, - }, - }, - togetherai: { - id: "togetherai", - env: ["TOGETHER_API_KEY"], - npm: "@ai-sdk/togetherai", - name: "Together AI", - doc: "https://docs.together.ai/docs/serverless-models", - models: { - "essentialai/Rnj-1-Instruct": { - id: "essentialai/Rnj-1-Instruct", - name: "Rnj-1 Instruct", - family: "rnj", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12-05", - last_updated: "2025-12-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.15 }, - limit: { context: 32768, output: 32768 }, - }, - "Qwen/Qwen3.5-397B-A17B": { - id: "Qwen/Qwen3.5-397B-A17B", - name: "Qwen3.5 397B A17B", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-16", - last_updated: "2026-02-16", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 3.6 }, - limit: { context: 262144, output: 130000 }, - }, - "Qwen/Qwen3.6-Plus": { - id: "Qwen/Qwen3.6-Plus", - name: "Qwen3.6 Plus", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-30", - last_updated: "2026-04-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 3 }, - limit: { context: 1000000, output: 500000 }, - }, - "Qwen/Qwen3-Coder-Next-FP8": { - id: "Qwen/Qwen3-Coder-Next-FP8", - name: "Qwen3 Coder Next FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2026-02-03", - release_date: "2026-02-03", - last_updated: "2026-02-03", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 1.2 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.6 }, - limit: { context: 262144, output: 262144 }, - }, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-23", - last_updated: "2025-07-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2, output: 2 }, - limit: { context: 262144, output: 262144 }, - }, - "zai-org/GLM-5.1": { - id: "zai-org/GLM-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-11", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.4, output: 4.4 }, - limit: { context: 202752, output: 131072 }, - }, - "meta-llama/Llama-3.3-70B-Instruct-Turbo": { - id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", - name: "Llama 3.3 70B", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-12", - release_date: "2024-12-06", - last_updated: "2024-12-06", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.88, output: 0.88 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek V3", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-07", - release_date: "2025-01-20", - last_updated: "2025-05-29", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1.25, output: 1.25 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-R1": { - id: "deepseek-ai/DeepSeek-R1", - name: "DeepSeek R1", - family: "deepseek-thinking", - attachment: false, - reasoning: true, - tool_call: false, - temperature: true, - knowledge: "2024-07", - release_date: "2024-12-26", - last_updated: "2025-03-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 3, output: 7 }, - limit: { context: 163839, output: 163839 }, - }, - "deepseek-ai/DeepSeek-V3-1": { - id: "deepseek-ai/DeepSeek-V3-1", - name: "DeepSeek V3.1", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-21", - last_updated: "2025-08-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.7 }, - limit: { context: 131072, output: 131072 }, - }, - "deepseek-ai/DeepSeek-V4-Pro": { - id: "deepseek-ai/DeepSeek-V4-Pro", - name: "DeepSeek V4 Pro", - family: "deepseek", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-24", - last_updated: "2026-04-24", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 2.1, output: 4.4, cache_read: 0.2 }, - limit: { context: 512000, output: 384000 }, - }, - "openai/gpt-oss-120b": { - id: "openai/gpt-oss-120b", - name: "GPT OSS 120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 131072, output: 131072 }, - }, - "google/gemma-4-31B-it": { - id: "google/gemma-4-31B-it", - name: "Gemma 4 31B Instruct", - family: "gemma", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-07", - last_updated: "2026-04-07", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.5 }, - limit: { context: 262144, output: 131072 }, - }, - "moonshotai/Kimi-K2.6": { - id: "moonshotai/Kimi-K2.6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 1.2, output: 4.5, cache_read: 0.2 }, - limit: { context: 262144, output: 131000 }, - }, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: true, - temperature: true, - knowledge: "2026-01", - release_date: "2026-01-27", - last_updated: "2026-01-27", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.5, output: 2.8 }, - limit: { context: 262144, output: 262144 }, - }, - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMaxAI/MiniMax-M2.7": { - id: "MiniMaxAI/MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06 }, - limit: { context: 202752, output: 131072 }, - }, - }, - }, - "qihang-ai": { - id: "qihang-ai", - env: ["QIHANG_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.qhaigc.net/v1", - name: "QiHang", - doc: "https://www.qhaigc.net/docs", - models: { - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.71, output: 3.57 }, - limit: { context: 200000, output: 32000 }, - }, - "gemini-3-flash-preview": { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.07, output: 0.43, context_over_200k: { input: 0.07, output: 0.43 } }, - limit: { context: 1048576, output: 65536 }, - }, - "gpt-5-mini": { - id: "gpt-5-mini", - name: "GPT-5-Mini", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-09-30", - release_date: "2025-09-15", - last_updated: "2025-09-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.04, output: 0.29 }, - limit: { context: 200000, output: 64000 }, - }, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview", - family: "gemini-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-11", - release_date: "2025-11-19", - last_updated: "2025-11-19", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.57, output: 3.43 }, - limit: { context: 1000000, output: 65000 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.43, output: 2.14 }, - limit: { context: 200000, output: 64000 }, - }, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 2 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2025-12-11", - last_updated: "2025-12-11", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 1.14 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "gemini-2.5-flash": { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash", - family: "gemini-flash", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2025-12-17", - last_updated: "2025-12-17", - modalities: { input: ["text", "image", "video", "audio", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.09, output: 0.71, context_over_200k: { input: 0.09, output: 0.71 } }, - limit: { context: 1048576, output: 65536 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-10-01", - last_updated: "2025-10-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.14, output: 0.71 }, - limit: { context: 200000, output: 64000 }, - }, - }, - }, - "tencent-tokenhub": { - id: "tencent-tokenhub", - env: ["TENCENT_TOKENHUB_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://tokenhub.tencentmaas.com/v1", - name: "Tencent TokenHub", - doc: "https://cloud.tencent.com/document/product/1823/130050", - models: { - "hy3-preview": { - id: "hy3-preview", - name: "Hy3 preview", - family: "Hy", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-04-20", - last_updated: "2026-04-20", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 256000, output: 64000 }, - }, - }, - }, - anthropic: { - id: "anthropic", - env: ["ANTHROPIC_API_KEY"], - npm: "@ai-sdk/anthropic", - name: "Anthropic", - doc: "https://docs.anthropic.com/en/docs/about-claude/models", - models: { - "claude-3-sonnet-20240229": { - id: "claude-3-sonnet-20240229", - name: "Claude Sonnet 3", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-04", - last_updated: "2024-03-04", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-haiku-4-5": { - id: "claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-5-20251101": { - id: "claude-opus-4-5-20251101", - name: "Claude Opus 4.5", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-01", - last_updated: "2025-11-01", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-opus-20240229": { - id: "claude-3-opus-20240229", - name: "Claude Opus 3", - family: "claude-opus", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-02-29", - last_updated: "2024-02-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-3-5-haiku-20241022": { - id: "claude-3-5-haiku-20241022", - name: "Claude Haiku 3.5", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-3-5-sonnet-20241022": { - id: "claude-3-5-sonnet-20241022", - name: "Claude Sonnet 3.5 v2", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-sonnet-4-6": { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 1000000, output: 64000 }, - }, - "claude-opus-4-0": { - id: "claude-opus-4-0", - name: "Claude Opus 4 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-opus-4-7": { - id: "claude-opus-4-7", - name: "Claude Opus 4.7", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - }, - "claude-3-haiku-20240307": { - id: "claude-3-haiku-20240307", - name: "Claude Haiku 3", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2023-08-31", - release_date: "2024-03-13", - last_updated: "2024-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }, - limit: { context: 200000, output: 4096 }, - }, - "claude-sonnet-4-5-20250929": { - id: "claude-sonnet-4-5-20250929", - name: "Claude Sonnet 4.5", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-haiku-latest": { - id: "claude-3-5-haiku-latest", - name: "Claude Haiku 3.5 (latest)", - family: "claude-haiku", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-07-31", - release_date: "2024-10-22", - last_updated: "2024-10-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-1": { - id: "claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-sonnet-4-0": { - id: "claude-sonnet-4-0", - name: "Claude Sonnet 4 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-3-5-sonnet-20240620": { - id: "claude-3-5-sonnet-20240620", - name: "Claude Sonnet 3.5", - family: "claude-sonnet", - attachment: true, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2024-04-30", - release_date: "2024-06-20", - last_updated: "2024-06-20", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 8192 }, - }, - "claude-opus-4-5": { - id: "claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-11-24", - last_updated: "2025-11-24", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-1-20250805": { - id: "claude-opus-4-1-20250805", - name: "Claude Opus 4.1", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - "claude-haiku-4-5-20251001": { - id: "claude-haiku-4-5-20251001", - name: "Claude Haiku 4.5", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2025-10-15", - last_updated: "2025-10-15", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-20250514": { - id: "claude-sonnet-4-20250514", - name: "Claude Sonnet 4", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-6": { - id: "claude-opus-4-6", - name: "Claude Opus 4.6", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-03-13", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }, - limit: { context: 1000000, output: 128000 }, - experimental: { - modes: { - fast: { - cost: { input: 30, output: 150, cache_read: 3, cache_write: 37.5 }, - provider: { body: { speed: "fast" }, headers: { "anthropic-beta": "fast-mode-2026-02-01" } }, - }, - }, - }, - }, - "claude-3-7-sonnet-20250219": { - id: "claude-3-7-sonnet-20250219", - name: "Claude Sonnet 3.7", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10-31", - release_date: "2025-02-19", - last_updated: "2025-02-19", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-sonnet-4-5": { - id: "claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2025-09-29", - last_updated: "2025-09-29", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 }, - limit: { context: 200000, output: 64000 }, - }, - "claude-opus-4-20250514": { - id: "claude-opus-4-20250514", - name: "Claude Opus 4", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2025-05-22", - last_updated: "2025-05-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 }, - limit: { context: 200000, output: 32000 }, - }, - }, - }, - modelscope: { - id: "modelscope", - env: ["MODELSCOPE_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api-inference.modelscope.cn/v1", - name: "ModelScope", - doc: "https://modelscope.cn/docs/model-service/API-Inference/intro", - models: { - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - id: "Qwen/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 32768 }, - }, - "Qwen/Qwen3-30B-A3B-Instruct-2507": { - id: "Qwen/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-30", - last_updated: "2025-07-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 16384 }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507", - name: "Qwen3 235B A22B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-04-28", - last_updated: "2025-07-21", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "Qwen/Qwen3-Coder-30B-A3B-Instruct": { - id: "Qwen/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2025-07-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "Qwen/Qwen3-235B-A22B-Thinking-2507": { - id: "Qwen/Qwen3-235B-A22B-Thinking-2507", - name: "Qwen3-235B-A22B-Thinking-2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-25", - last_updated: "2025-07-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "ZhipuAI/GLM-4.5": { - id: "ZhipuAI/GLM-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "ZhipuAI/GLM-4.6": { - id: "ZhipuAI/GLM-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 202752, output: 98304 }, - }, - }, - }, - "hpc-ai": { - id: "hpc-ai", - env: ["HPC_AI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.hpc-ai.com/inference/v1", - name: "HPC-AI", - doc: "https://www.hpc-ai.com/doc/docs/quickstart/", - models: { - "zai-org/glm-5.1": { - id: "zai-org/glm-5.1", - name: "GLM 5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-04-08", - last_updated: "2026-04-08", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.66, output: 2, cache_read: 0.12 }, - limit: { context: 202000, output: 202000 }, - }, - "minimax/minimax-m2.5": { - id: "minimax/minimax-m2.5", - name: "MiniMax M2.5", - family: "minimax-m2.5", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: false, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-03-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.14, output: 0.56, cache_read: 0.014 }, - limit: { context: 1000000, output: 131072 }, - }, - "moonshotai/kimi-k2.5": { - id: "moonshotai/kimi-k2.5", - name: "Kimi K2.5", - family: "kimi", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: false, - knowledge: "2025-01-01", - release_date: "2026-01-01", - last_updated: "2026-03-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.21, output: 1, cache_read: 0.03 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - gitlab: { - id: "gitlab", - env: ["GITLAB_TOKEN"], - npm: "gitlab-ai-provider", - name: "GitLab Duo", - doc: "https://docs.gitlab.com/user/duo_agent_platform/", - models: { - "duo-chat-gpt-5-4-nano": { - id: "duo-chat-gpt-5-4-nano", - name: "Agentic Chat (GPT-5.4 Nano)", - family: "gpt-nano", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-mini": { - id: "duo-chat-gpt-5-mini", - name: "Agentic Chat (GPT-5 Mini)", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-05-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-sonnet-4-6": { - id: "duo-chat-sonnet-4-6", - name: "Agentic Chat (Claude Sonnet 4.6)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-08-31", - release_date: "2026-02-17", - last_updated: "2026-02-17", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - "duo-chat-gpt-5-2": { - id: "duo-chat-gpt-5-2", - name: "Agentic Chat (GPT-5.2)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-23", - last_updated: "2026-01-23", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-codex": { - id: "duo-chat-gpt-5-codex", - name: "Agentic Chat (GPT-5 Codex)", - family: "gpt-codex", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-1": { - id: "duo-chat-gpt-5-1", - name: "Agentic Chat (GPT-5.1)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2024-09-30", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-2-codex": { - id: "duo-chat-gpt-5-2-codex", - name: "Agentic Chat (GPT-5.2 Codex)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-01-22", - last_updated: "2026-01-22", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-sonnet-4-5": { - id: "duo-chat-sonnet-4-5", - name: "Agentic Chat (Claude Sonnet 4.5)", - family: "claude-sonnet", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-07-31", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-gpt-5-4": { - id: "duo-chat-gpt-5-4", - name: "Agentic Chat (GPT-5.4)", - family: "gpt", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-05", - last_updated: "2026-03-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 1050000, input: 922000, output: 128000 }, - }, - "duo-chat-haiku-4-5": { - id: "duo-chat-haiku-4-5", - name: "Agentic Chat (Claude Haiku 4.5)", - family: "claude-haiku", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-02-28", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-gpt-5-3-codex": { - id: "duo-chat-gpt-5-3-codex", - name: "Agentic Chat (GPT-5.3 Codex)", - family: "gpt-codex", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-gpt-5-4-mini": { - id: "duo-chat-gpt-5-4-mini", - name: "Agentic Chat (GPT-5.4 Mini)", - family: "gpt-mini", - attachment: true, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: false, - knowledge: "2025-08-31", - release_date: "2026-03-17", - last_updated: "2026-03-17", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0 }, - limit: { context: 400000, input: 272000, output: 128000 }, - }, - "duo-chat-opus-4-7": { - id: "duo-chat-opus-4-7", - name: "Agentic Chat (Claude Opus 4.7)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: false, - knowledge: "2026-01-31", - release_date: "2026-04-16", - last_updated: "2026-04-16", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - "duo-chat-opus-4-5": { - id: "duo-chat-opus-4-5", - name: "Agentic Chat (Claude Opus 4.5)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-03-31", - release_date: "2026-01-08", - last_updated: "2026-01-08", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 64000 }, - }, - "duo-chat-opus-4-6": { - id: "duo-chat-opus-4-6", - name: "Agentic Chat (Claude Opus 4.6)", - family: "claude-opus", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-05-31", - release_date: "2026-02-05", - last_updated: "2026-02-05", - modalities: { input: ["text", "image", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, - xiaomi: { - id: "xiaomi", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.xiaomimimo.com/v1", - name: "Xiaomi", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0.4, output: 2, cache_read: 0.08 }, - limit: { context: 262144, output: 131072 }, - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { - input: 0.4, - output: 2, - cache_read: 0.08, - context_over_200k: { input: 0.8, output: 4, cache_read: 0.16 }, - }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 3, cache_read: 0.2, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.3, cache_read: 0.01 }, - limit: { context: 262144, output: 65536 }, - }, - }, - }, - clarifai: { - id: "clarifai", - env: ["CLARIFAI_PAT"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.clarifai.com/v2/ext/openai/v1", - name: "Clarifai", - doc: "https://docs.clarifai.com/compute/inference/", - models: { - "arcee_ai/AFM/models/trinity-mini": { - id: "arcee_ai/AFM/models/trinity-mini", - name: "Trinity Mini", - family: "trinity-mini", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2024-10", - release_date: "2025-12", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.15 }, - limit: { context: 131072, output: 131072 }, - }, - "mistralai/completion/models/Ministral-3-14B-Reasoning-2512": { - id: "mistralai/completion/models/Ministral-3-14B-Reasoning-2512", - name: "Ministral 3 14B Reasoning 2512", - family: "ministral", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-12", - release_date: "2025-12-01", - last_updated: "2025-12-12", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 2.5, output: 1.7 }, - limit: { context: 262144, output: 262144 }, - }, - "mistralai/completion/models/Ministral-3-3B-Reasoning-2512": { - id: "mistralai/completion/models/Ministral-3-3B-Reasoning-2512", - name: "Ministral 3 3B Reasoning 2512", - family: "ministral", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12", - last_updated: "2026-02-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 1.039, output: 0.54825 }, - limit: { context: 262144, output: 262144 }, - }, - "deepseek-ai/deepseek-ocr/models/DeepSeek-OCR": { - id: "deepseek-ai/deepseek-ocr/models/DeepSeek-OCR", - name: "DeepSeek OCR", - family: "deepseek", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-10-20", - last_updated: "2026-02-25", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 0.7 }, - limit: { context: 8192, output: 8192 }, - }, - "openai/chat-completion/models/gpt-oss-20b": { - id: "openai/chat-completion/models/gpt-oss-20b", - name: "GPT OSS 20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-12-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.045, output: 0.18 }, - limit: { context: 131072, output: 16384 }, - }, - "openai/chat-completion/models/gpt-oss-120b-high-throughput": { - id: "openai/chat-completion/models/gpt-oss-120b-high-throughput", - name: "GPT OSS 120B High Throughput", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.09, output: 0.36 }, - limit: { context: 131072, output: 16384 }, - }, - "minimaxai/chat-completion/models/MiniMax-M2_5-high-throughput": { - id: "minimaxai/chat-completion/models/MiniMax-M2_5-high-throughput", - name: "MiniMax-M2.5 High Throughput", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "qwen/qwenCoder/models/Qwen3-Coder-30B-A3B-Instruct": { - id: "qwen/qwenCoder/models/Qwen3-Coder-30B-A3B-Instruct", - name: "Qwen3 Coder 30B A3B Instruct", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-31", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.11458, output: 0.74812 }, - limit: { context: 262144, output: 65536 }, - }, - "qwen/qwenLM/models/Qwen3-30B-A3B-Thinking-2507": { - id: "qwen/qwenLM/models/Qwen3-30B-A3B-Thinking-2507", - name: "Qwen3 30B A3B Thinking 2507", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-31", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.36, output: 1.3 }, - limit: { context: 262144, output: 131072 }, - }, - "qwen/qwenLM/models/Qwen3-30B-A3B-Instruct-2507": { - id: "qwen/qwenLM/models/Qwen3-30B-A3B-Instruct-2507", - name: "Qwen3 30B A3B Instruct 2507", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: true, - structured_output: true, - temperature: true, - release_date: "2025-07-30", - last_updated: "2026-02-25", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.5 }, - limit: { context: 262144, output: 262144 }, - }, - "clarifai/main/models/mm-poly-8b": { - id: "clarifai/main/models/mm-poly-8b", - name: "MM Poly 8B", - family: "mm-poly", - attachment: true, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2025-06", - last_updated: "2026-02-25", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: false, - cost: { input: 0.658, output: 1.11 }, - limit: { context: 32768, output: 4096 }, - }, - "moonshotai/chat-completion/models/Kimi-K2_6": { - id: "moonshotai/chat-completion/models/Kimi-K2_6", - name: "Kimi K2.6", - family: "kimi-k2.6", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - knowledge: "2025-01", - release_date: "2026-04-21", - last_updated: "2026-04-21", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.95, output: 4 }, - limit: { context: 262144, output: 262144 }, - }, - }, - }, - "minimax-cn": { - id: "minimax-cn", - env: ["MINIMAX_API_KEY"], - npm: "@ai-sdk/anthropic", - api: "https://api.minimaxi.com/anthropic/v1", - name: "MiniMax (minimaxi.com)", - doc: "https://platform.minimaxi.com/docs/guides/quickstart", - models: { - "MiniMax-M2": { - id: "MiniMax-M2", - name: "MiniMax-M2", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-10-27", - last_updated: "2025-10-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 196608, output: 128000 }, - }, - "MiniMax-M2.5": { - id: "MiniMax-M2.5", - name: "MiniMax-M2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-12", - last_updated: "2026-02-12", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.03, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7": { - id: "MiniMax-M2.7", - name: "MiniMax-M2.7", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.7-highspeed": { - id: "MiniMax-M2.7-highspeed", - name: "MiniMax-M2.7-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.1": { - id: "MiniMax-M2.1", - name: "MiniMax-M2.1", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-23", - last_updated: "2025-12-23", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 204800, output: 131072 }, - }, - "MiniMax-M2.5-highspeed": { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax-M2.5-highspeed", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-13", - last_updated: "2026-02-13", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.4, cache_read: 0.06, cache_write: 0.375 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - "regolo-ai": { - id: "regolo-ai", - env: ["REGOLO_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.regolo.ai/v1", - name: "Regolo AI", - doc: "https://docs.regolo.ai/", - models: { - "mistral-small3.2": { - id: "mistral-small3.2", - name: "Mistral Small 3.2", - family: "mistral-small", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-01-31", - last_updated: "2025-01-31", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.5, output: 2.2 }, - limit: { context: 120000, output: 120000 }, - }, - "qwen3-embedding-8b": { - id: "qwen3-embedding-8b", - name: "Qwen3-Embedding-8B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.1, output: 0.1 }, - limit: { context: 32768, output: 8192 }, - }, - "llama-3.3-70b-instruct": { - id: "llama-3.3-70b-instruct", - name: "Llama 3.3 70B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-28", - last_updated: "2025-04-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.6, output: 2.7 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3-reranker-4b": { - id: "qwen3-reranker-4b", - name: "Qwen3-Reranker-4B", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: false, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.12, output: 0.12 }, - limit: { context: 32768, output: 8192 }, - }, - "mistral-small-4-119b": { - id: "mistral-small-4-119b", - name: "Mistral Small 4 119B", - family: "mistral-small", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-15", - last_updated: "2026-03-15", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: false, - cost: { input: 0.75, output: 3 }, - limit: { context: 256000, output: 16384 }, - }, - "qwen3.5-122b": { - id: "qwen3.5-122b", - name: "Qwen3.5-122B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.9, output: 3.6 }, - limit: { context: 262144, output: 16384 }, - }, - "qwen-image": { - id: "qwen-image", - name: "Qwen-Image", - family: "qwen", - attachment: false, - reasoning: false, - tool_call: false, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text"], output: ["image"] }, - open_weights: false, - cost: { input: 0.5, output: 2 }, - limit: { context: 8192, output: 4096 }, - }, - "qwen3-coder-next": { - id: "qwen3-coder-next", - name: "Qwen3-Coder-Next", - family: "qwen", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 1.2 }, - limit: { context: 262144, output: 16384 }, - }, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax 2.5", - family: "minimax", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-10", - last_updated: "2026-03-10", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.8, output: 3.5 }, - limit: { context: 190000, output: 64000 }, - }, - "gpt-oss-20b": { - id: "gpt-oss-20b", - name: "GPT-OSS-20B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-03-01", - last_updated: "2026-03-01", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.4, output: 1.8 }, - limit: { context: 128000, output: 16384 }, - }, - "qwen3.5-9b": { - id: "qwen3.5-9b", - name: "Qwen3.5-9B", - family: "qwen", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2026-02-01", - last_updated: "2026-02-01", - modalities: { input: ["text", "image"], output: ["text"] }, - open_weights: true, - cost: { input: 0.15, output: 0.6 }, - limit: { context: 262144, output: 8192 }, - }, - "gpt-oss-120b": { - id: "gpt-oss-120b", - name: "GPT-OSS-120B", - family: "gpt-oss", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-08-05", - last_updated: "2025-08-05", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 1, output: 4.2 }, - limit: { context: 128000, output: 16384 }, - }, - "llama-3.1-8b-instruct": { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - family: "llama", - attachment: false, - reasoning: false, - tool_call: true, - temperature: true, - release_date: "2025-04-07", - last_updated: "2025-04-07", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0.05, output: 0.25 }, - limit: { context: 120000, output: 120000 }, - }, - }, - }, - "xiaomi-token-plan-ams": { - id: "xiaomi-token-plan-ams", - env: ["XIAOMI_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://token-plan-ams.xiaomimimo.com/v1", - name: "Xiaomi Token Plan (Europe)", - doc: "https://platform.xiaomimimo.com/#/docs", - models: { - "mimo-v2-tts": { - id: "mimo-v2-tts", - name: "MiMo-V2-TTS", - family: "mimo", - attachment: false, - reasoning: false, - tool_call: false, - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["audio"] }, - open_weights: true, - cost: { input: 0, output: 0 }, - limit: { context: 8192, output: 16384 }, - }, - "mimo-v2-flash": { - id: "mimo-v2-flash", - name: "MiMo-V2-Flash", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12-01", - release_date: "2025-12-16", - last_updated: "2026-02-04", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 65536 }, - }, - "mimo-v2-pro": { - id: "mimo-v2-pro", - name: "MiMo-V2-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 2, output: 6, cache_read: 0.4 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2.5": { - id: "mimo-v2.5", - name: "MiMo-V2.5", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text", "image", "audio", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - "mimo-v2-omni": { - id: "mimo-v2-omni", - name: "MiMo-V2-Omni", - family: "mimo", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-03-18", - last_updated: "2026-03-18", - modalities: { input: ["text", "image", "audio", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, cache_read: 0 }, - limit: { context: 262144, output: 131072 }, - }, - "mimo-v2.5-pro": { - id: "mimo-v2.5-pro", - name: "MiMo-V2.5-Pro", - family: "mimo", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2024-12", - release_date: "2026-04-22", - last_updated: "2026-04-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, context_over_200k: { input: 0, output: 0, cache_read: 0 } }, - limit: { context: 1048576, output: 131072 }, - }, - }, - }, - zhipuai: { - id: "zhipuai", - env: ["ZHIPU_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://open.bigmodel.cn/api/paas/v4", - name: "Zhipu AI", - doc: "https://docs.z.ai/guides/overview/pricing", - models: { - "glm-5v-turbo": { - id: "glm-5v-turbo", - name: "GLM-5V-Turbo", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-04-01", - last_updated: "2026-04-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 5, output: 22, cache_read: 1.2, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-5": { - id: "glm-5", - name: "GLM-5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - release_date: "2026-02-11", - last_updated: "2026-02-11", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 1, output: 3.2, cache_read: 0.2, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-5.1": { - id: "glm-5.1", - name: "GLM-5.1", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - structured_output: true, - temperature: true, - release_date: "2026-03-27", - last_updated: "2026-03-27", - modalities: { input: ["text"], output: ["text"] }, - open_weights: false, - cost: { input: 6, output: 24, cache_read: 1.3, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7-flash": { - id: "glm-4.7-flash", - name: "GLM-4.7-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.5-flash": { - id: "glm-4.5-flash", - name: "GLM-4.5-Flash", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.6v": { - id: "glm-4.6v", - name: "GLM-4.6V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-08", - last_updated: "2025-12-08", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.3, output: 0.9 }, - limit: { context: 128000, output: 32768 }, - }, - "glm-4.6": { - id: "glm-4.6", - name: "GLM-4.6", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-09-30", - last_updated: "2025-09-30", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - "glm-4.5v": { - id: "glm-4.5v", - name: "GLM-4.5V", - family: "glm", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-08-11", - last_updated: "2025-08-11", - modalities: { input: ["text", "image", "video"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 1.8 }, - limit: { context: 64000, output: 16384 }, - }, - "glm-4.5-air": { - id: "glm-4.5-air", - name: "GLM-4.5-Air", - family: "glm-air", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.2, output: 1.1, cache_read: 0.03, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.5": { - id: "glm-4.5", - name: "GLM-4.5", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2025-07-28", - last_updated: "2025-07-28", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 131072, output: 98304 }, - }, - "glm-4.7-flashx": { - id: "glm-4.7-flashx", - name: "GLM-4.7-FlashX", - family: "glm-flash", - attachment: false, - reasoning: true, - tool_call: true, - temperature: true, - knowledge: "2025-04", - release_date: "2026-01-19", - last_updated: "2026-01-19", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.07, output: 0.4, cache_read: 0.01, cache_write: 0 }, - limit: { context: 200000, output: 131072 }, - }, - "glm-4.7": { - id: "glm-4.7", - name: "GLM-4.7", - family: "glm", - attachment: false, - reasoning: true, - tool_call: true, - interleaved: { field: "reasoning_content" }, - temperature: true, - knowledge: "2025-04", - release_date: "2025-12-22", - last_updated: "2025-12-22", - modalities: { input: ["text"], output: ["text"] }, - open_weights: true, - cost: { input: 0.6, output: 2.2, cache_read: 0.11, cache_write: 0 }, - limit: { context: 204800, output: 131072 }, - }, - }, - }, - nova: { - id: "nova", - env: ["NOVA_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.nova.amazon.com/v1", - name: "Nova", - doc: "https://nova.amazon.com/dev/documentation", - models: { - "nova-2-lite-v1": { - id: "nova-2-lite-v1", - name: "Nova 2 Lite", - family: "nova-lite", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-01", - last_updated: "2025-12-01", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, reasoning: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - "nova-2-pro-v1": { - id: "nova-2-pro-v1", - name: "Nova 2 Pro", - family: "nova-pro", - attachment: true, - reasoning: true, - tool_call: true, - temperature: true, - release_date: "2025-12-03", - last_updated: "2026-01-03", - modalities: { input: ["text", "image", "video", "pdf"], output: ["text"] }, - open_weights: false, - cost: { input: 0, output: 0, reasoning: 0 }, - limit: { context: 1000000, output: 64000 }, - }, - }, - }, -} diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts new file mode 100644 index 00000000000..ec8038f7134 --- /dev/null +++ b/packages/core/src/permission.ts @@ -0,0 +1,45 @@ +export * as PermissionV2 from "./permission" + +import { Schema } from "effect" +import { Wildcard } from "./util/wildcard" + +export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Action" }) +export type Action = typeof Action.Type + +export const Rule = Schema.Struct({ + permission: Schema.String, + pattern: Schema.String, + action: Action, +}).annotate({ identifier: "PermissionV2.Rule" }) +export type Rule = typeof Rule.Type + +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) +export type Ruleset = typeof Ruleset.Type + +const EDIT_TOOLS = ["edit", "write", "apply_patch"] + +export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { + return ( + rulesets + .flat() + .findLast((rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern)) ?? { + action: "ask", + permission, + pattern: "*", + } + ) +} + +export function merge(...rulesets: Ruleset[]): Ruleset { + return rulesets.flat() +} + +export function disabled(tools: string[], ruleset: Ruleset): Set { + return new Set( + tools.filter((tool) => { + const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool + const rule = ruleset.findLast((rule) => Wildcard.match(permission, rule.permission)) + return rule?.pattern === "*" && rule.action === "deny" + }), + ) +} diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index dfcae946859..df8a40c1fa3 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -2,27 +2,35 @@ export * as PluginV2 from "./plugin" import { createDraft, finishDraft, type Draft } from "immer" import type { LanguageModelV3 } from "@ai-sdk/provider" -import { type ProviderV2 } from "./provider" -import { Context, Effect, Layer, Schema } from "effect" +import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" import type { ModelV2 } from "./model" +import type { Catalog } from "./catalog" +import { EventV2 } from "./event" export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export type ID = typeof ID.Type +export const Event = { + Added: EventV2.define({ + type: "plugin.added", + schema: { + id: ID, + }, + }), +} + type HookSpec = { - "provider.update": { - input: {} - output: { - provider: ProviderV2.Info - cancel: boolean - } + "catalog.transform": { + input: Catalog.Editor + output: {} } - "model.update": { - input: {} - output: { - model: ModelV2.Info - cancel: boolean + "account.switched": { + input: { + serviceID: import("./account").AccountV2.ServiceID + from?: import("./account").AccountV2.ID + to?: import("./account").AccountV2.ID } + output: {} } "aisdk.language": { input: { @@ -61,15 +69,24 @@ export type HookFunctions = { export type HookInput = HookSpec[Name]["input"] export type HookOutput = HookSpec[Name]["output"] -export type Effect = Effect.Effect +export type Effect = Effect.Effect export function define(input: { id: ID; effect: Effect.Effect }) { return input } export interface Interface { - readonly add: (input: { id: ID; effect: Effect }) => Effect.Effect + readonly add: (input: { + id: ID + effect: Effect.Effect + }) => Effect.Effect readonly remove: (id: ID) => Effect.Effect + readonly triggerFor: ( + id: ID, + name: Name, + input: HookInput, + output: HookOutput, + ) => Effect.Effect & HookOutput> readonly trigger: ( name: Name, input: HookInput, @@ -85,21 +102,30 @@ export const layer = Layer.effect( let hooks: { id: ID hooks: HookFunctions + scope: Scope.Closeable }[] = [] + const events = yield* EventV2.Service const svc = Service.of({ add: Effect.fn("Plugin.add")(function* (input) { - const result = yield* input.effect - if (!result) return + const existing = hooks.find((item) => item.id === input.id) + if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) + const scope = yield* Scope.make() + const result = yield* input.effect.pipe(Scope.provide(scope)) hooks = [ ...hooks.filter((item) => item.id !== input.id), { id: input.id, - hooks: result, + hooks: result ?? {}, + scope, }, ] + yield* events.publish(Event.Added, { id: input.id }) }), trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) { + return yield* svc.triggerFor(ID.make("*"), name, input, output) + }), + triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) { const draftEntries = new Map>() const event = { ...input, @@ -114,6 +140,7 @@ export const layer = Layer.effect( } for (const item of hooks) { + if (id !== ID.make("*") && item.id !== id) continue const match = item.hooks[name] if (!match) continue yield* match(event as any).pipe( @@ -133,14 +160,16 @@ export const layer = Layer.effect( return event as any }), remove: Effect.fn("Plugin.remove")(function* (id) { + const existing = hooks.find((item) => item.id === id) hooks = hooks.filter((item) => item.id !== id) + if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore) }), }) return svc }), ) -export const defaultLayer = layer +export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer)) // opencode // sdcok diff --git a/packages/core/src/plugin/account.ts b/packages/core/src/plugin/account.ts new file mode 100644 index 00000000000..71e64bf23f0 --- /dev/null +++ b/packages/core/src/plugin/account.ts @@ -0,0 +1,48 @@ +import { Effect, Scope, Stream } from "effect" +import { AccountV2 } from "../account" +import { EventV2 } from "../event" +import { PluginV2 } from "../plugin" + +export const AccountPlugin = PluginV2.define({ + id: PluginV2.ID.make("account"), + effect: Effect.gen(function* () { + const accounts = yield* AccountV2.Service + const events = yield* EventV2.Service + const scope = yield* Scope.Scope + + yield* events.subscribe(AccountV2.Event.Switched).pipe( + Stream.runForEach((event) => + PluginV2.Service.use((plugin) => plugin.trigger("account.switched", event.data, {})).pipe(Effect.asVoid), + ), + Effect.forkIn(scope, { startImmediately: true }), + ) + + return { + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + const account = yield* accounts.active(AccountV2.ServiceID.make(item.provider.id)).pipe(Effect.orDie) + if (!account) continue + evt.provider.update(item.provider.id, (provider) => { + provider.enabled = { + via: "account", + service: account.serviceID, + } + if (account.credential.type === "api") { + provider.options.aisdk.provider.apiKey = account.credential.key + Object.assign(provider.options.aisdk.provider, account.credential.metadata ?? {}) + } + if (account.credential.type === "oauth") { + provider.options.aisdk.provider.apiKey = account.credential.access + // kilocode_change start + if (provider.id === "kilo" && account.credential.accountId) { + provider.options.aisdk.provider.kilocodeOrganizationId = account.credential.accountId + } + // kilocode_change end + } + }) + } + }), + "account.switched": Effect.fn(function* () {}), + } + }), +}) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts new file mode 100644 index 00000000000..9baba75c386 --- /dev/null +++ b/packages/core/src/plugin/agent.ts @@ -0,0 +1,211 @@ +export * as AgentPlugin from "./agent" + +import path from "path" +import { Effect } from "effect" +import { AgentV2 } from "../agent" +import { Global } from "../global" +import { Location } from "../location" +import { PermissionV2 } from "../permission" +import { PluginV2 } from "../plugin" + +const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") + +const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases. + +Your strengths: +- Rapidly finding files using glob patterns +- Searching code and text with powerful regex patterns +- Reading and analyzing file contents + +Guidelines: +- Use Glob for broad file pattern matching +- Use Grep for searching file contents with regex +- Use Read when you know the specific file path you need to read +- Use Bash for file operations like copying, moving, or listing directory contents +- Adapt your search approach based on the thoroughness level specified by the caller +- Return file paths as absolute paths in your final response +- For clear communication, avoid using emojis +- Do not create any files, or run bash commands that modify the user's system state in any way + +Complete the user's search request efficiently and report your findings clearly.` + +const PROMPT_COMPACTION = `You are an anchored context summarization assistant for coding sessions. + +Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work. + +If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. + +Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. + +Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.` + +const PROMPT_TITLE = `You are a title generator. You output ONLY a thread title. Nothing else. + + +Generate a brief title that would help the user find this conversation later. + +Follow all rules in +Use the so you know what a good title looks like. +Your output must be: +- A single line +- <=50 characters +- No explanations + + + +- you MUST use the same language as the user message you are summarizing +- Title must be grammatically correct and read naturally - no word salad +- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool") +- Focus on the main topic or question the user needs to retrieve +- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing" +- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it +- Keep exact: technical terms, numbers, filenames, HTTP codes +- Remove: the, this, my, a, an +- Never assume tech stack +- Never use tools +- NEVER respond to questions, just generate a title for the conversation +- The title should NEVER include "summarizing" or "generating" when generating a title +- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT +- Always output something meaningful, even if the input is minimal. +- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"): + -> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) + + + +"debug 500 errors in production" -> Debugging production 500 errors +"refactor user service" -> Refactoring user service +"why is app.js failing" -> app.js failure investigation +"implement rate limiting" -> Rate limiting implementation +"how do I connect postgres to my API" -> Postgres API connection +"best practices for React hooks" -> React hooks best practices +"@src/auth.ts can you add refresh token support" -> Auth refresh token support +"@utils/parser.ts this is broken" -> Parser bug fix +"look at @config.json" -> Config review +"@App.tsx add dark mode toggle" -> Dark mode toggle in App +` + +const PROMPT_SUMMARY = `Summarize what was done in this conversation. Write like a pull request description. + +Rules: +- 2-3 sentences max +- Describe the changes made, not the process +- Do not mention running tests, builds, or other validation steps +- Do not explain what the user asked for +- Write in first person (I added..., I fixed...) +- Never ask questions or add new questions +- If the conversation ends with an unanswered question to the user, preserve that exact question +- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary` + +export const Plugin = PluginV2.define({ + id: PluginV2.ID.make("agent"), + effect: Effect.gen(function* () { + const agent = yield* AgentV2.Service + const location = yield* Location.Service + const worktree = location.directory + const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")] + const readonlyExternalDirectory: PermissionV2.Ruleset = [ + { permission: "external_directory", pattern: "*", action: "ask" }, + ...whitelistedDirs.map( + (pattern): PermissionV2.Rule => ({ permission: "external_directory", pattern, action: "allow" }), + ), + ] + const defaults: PermissionV2.Ruleset = [ + { permission: "*", pattern: "*", action: "allow" }, + ...readonlyExternalDirectory, + { permission: "question", pattern: "*", action: "deny" }, + { permission: "plan_enter", pattern: "*", action: "deny" }, + { permission: "plan_exit", pattern: "*", action: "deny" }, + { permission: "repo_clone", pattern: "*", action: "deny" }, + { permission: "repo_overview", pattern: "*", action: "deny" }, + { permission: "read", pattern: "*", action: "allow" }, + { permission: "read", pattern: "*.env", action: "ask" }, + { permission: "read", pattern: "*.env.*", action: "ask" }, + { permission: "read", pattern: "*.env.example", action: "allow" }, + ] + + yield* agent.update((editor) => { + editor.update(AgentV2.ID.make("build"), (item) => { + item.description = "The default agent. Executes tools based on configured permissions." + item.mode = "primary" + item.permissions.push( + ...PermissionV2.merge(defaults, [ + { permission: "question", pattern: "*", action: "allow" }, + { permission: "plan_enter", pattern: "*", action: "allow" }, + ]), + ) + }) + + editor.update(AgentV2.ID.make("plan"), (item) => { + item.description = "Plan mode. Disallows all edit tools." + item.mode = "primary" + item.permissions.push( + ...PermissionV2.merge(defaults, [ + { permission: "question", pattern: "*", action: "allow" }, + { permission: "plan_exit", pattern: "*", action: "allow" }, + { permission: "external_directory", pattern: path.join(Global.Path.data, "plans", "*"), action: "allow" }, + { permission: "edit", pattern: "*", action: "deny" }, + { permission: "edit", pattern: path.join(".opencode", "plans", "*.md"), action: "allow" }, + { + permission: "edit", + pattern: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")), + action: "allow", + }, + ]), + ) + }) + + editor.update(AgentV2.ID.make("general"), (item) => { + item.description = + "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." + item.mode = "subagent" + item.permissions.push( + ...PermissionV2.merge(defaults, [{ permission: "todowrite", pattern: "*", action: "deny" }]), + ) + }) + + editor.update(AgentV2.ID.make("explore"), (item) => { + item.description = + 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' + item.system = PROMPT_EXPLORE + item.mode = "subagent" + item.permissions.push( + ...PermissionV2.merge( + defaults, + [ + { permission: "*", pattern: "*", action: "deny" }, + { permission: "grep", pattern: "*", action: "allow" }, + { permission: "glob", pattern: "*", action: "allow" }, + { permission: "list", pattern: "*", action: "allow" }, + { permission: "bash", pattern: "*", action: "allow" }, + { permission: "webfetch", pattern: "*", action: "allow" }, + { permission: "websearch", pattern: "*", action: "allow" }, + { permission: "read", pattern: "*", action: "allow" }, + ], + readonlyExternalDirectory, + ), + ) + }) + + editor.update(AgentV2.ID.make("compaction"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_COMPACTION + item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + }) + + editor.update(AgentV2.ID.make("title"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_TITLE + item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + }) + + editor.update(AgentV2.ID.make("summary"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_SUMMARY + item.permissions.push(...PermissionV2.merge(defaults, [{ permission: "*", pattern: "*", action: "deny" }])) + }) + }) + }), +}) diff --git a/packages/core/src/plugin/auth.ts b/packages/core/src/plugin/auth.ts deleted file mode 100644 index 503d82852cc..00000000000 --- a/packages/core/src/plugin/auth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Effect } from "effect" -import { AuthV2 } from "../auth" -import { PluginV2 } from "../plugin" - -export const AuthPlugin = PluginV2.define({ - id: PluginV2.ID.make("auth"), - effect: Effect.gen(function* () { - const auth = yield* AuthV2.Service - return { - "provider.update": Effect.fn(function* (evt) { - const account = yield* auth.active(AuthV2.ServiceID.make(evt.provider.id)).pipe(Effect.orDie) - if (!account) return - evt.provider.enabled = { - via: "auth", - service: account.serviceID, - } - if (account.credential.type === "api") { - evt.provider.options.aisdk.provider.apiKey = account.credential.key - Object.assign(evt.provider.options.aisdk.provider, account.credential.metadata ?? {}) - } - if (account.credential.type === "oauth") { - evt.provider.options.aisdk.provider.apiKey = account.credential.access - if (evt.provider.id === "kilo" && account.credential.accountId) { - evt.provider.options.aisdk.provider.kilocodeOrganizationId = account.credential.accountId // kilocode_change - } - } - }), - } - }), -}) diff --git a/packages/core/src/plugin/boot.ts b/packages/core/src/plugin/boot.ts index 74560ac85b7..4456ac19085 100644 --- a/packages/core/src/plugin/boot.ts +++ b/packages/core/src/plugin/boot.ts @@ -1,18 +1,34 @@ export * as PluginBoot from "./boot" import { Context, Deferred, Effect, Layer } from "effect" -import { AuthV2 } from "../auth" +import { AccountV2 } from "../account" +import { AgentV2 } from "../agent" import { Catalog } from "../catalog" +import { Config } from "../config" +import { ConfigAgentPlugin } from "../config/plugin/agent" +import { EventV2 } from "../event" +import { Location } from "../location" import { Npm } from "../npm" import { PluginV2 } from "../plugin" -import { AuthPlugin } from "./auth" +import { AccountPlugin } from "./account" +import { AgentPlugin } from "./agent" +import { ConfigProviderPlugin } from "../config/plugin/provider" import { EnvPlugin } from "./env" import { ModelsDevPlugin } from "./models-dev" import { ProviderPlugins } from "./provider" type Plugin = { id: PluginV2.ID - effect: Effect.Effect + effect: PluginV2.Effect< + | Catalog.Service + | AccountV2.Service + | AgentV2.Service + | Npm.Service + | EventV2.Service + | Location.Service + | PluginV2.Service + | Config.Service + > } export interface Interface { @@ -21,51 +37,65 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/PluginBoot") {} -export const layer: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const catalog = yield* Catalog.Service - const plugin = yield* PluginV2.Service - const auth = yield* AuthV2.Service - const npm = yield* Npm.Service - const done = yield* Deferred.make() +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const plugin = yield* PluginV2.Service + const accounts = yield* AccountV2.Service + const agents = yield* AgentV2.Service + const config = yield* Config.Service + const location = yield* Location.Service + const npm = yield* Npm.Service + const events = yield* EventV2.Service + const done = yield* Deferred.make() - const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) { - yield* plugin.add({ - id: input.id, - effect: input.effect.pipe( - Effect.provideService(Catalog.Service, catalog), - Effect.provideService(AuthV2.Service, auth), - Effect.provideService(Npm.Service, npm), - ), - }) + const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) { + yield* plugin.add({ + id: input.id, + effect: input.effect.pipe( + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(AgentV2.Service, agents), + Effect.provideService(Config.Service, config), + Effect.provideService(Location.Service, location), + Effect.provideService(Npm.Service, npm), + Effect.provideService(EventV2.Service, events), + Effect.provideService(PluginV2.Service, plugin), + ), }) + }) - const boot = Effect.gen(function* () { - yield* add(EnvPlugin) - yield* add(AuthPlugin) - for (const item of ProviderPlugins) { - yield* add(item) - } - yield* add(ModelsDevPlugin) - }).pipe(Effect.withSpan("PluginBoot.boot")) + const boot = Effect.gen(function* () { + yield* add(EnvPlugin) + yield* add(AccountPlugin) + yield* add(AgentPlugin.Plugin) + for (const item of ProviderPlugins) { + yield* add(item) + } + yield* add(ModelsDevPlugin) + yield* add(ConfigProviderPlugin.Plugin) + yield* add(ConfigAgentPlugin.Plugin) + }).pipe(Effect.withSpan("PluginBoot.boot")) - yield* boot.pipe( - Effect.exit, - Effect.flatMap((exit) => Deferred.done(done, exit)), - Effect.forkScoped, - ) + yield* boot.pipe( + Effect.exit, + Effect.flatMap((exit) => Deferred.done(done, exit)), + Effect.forkScoped, + ) - return Service.of({ - wait: () => Deferred.await(done), - }) - }), - ) + return Service.of({ + wait: () => Deferred.await(done), + }) + }), +) export const defaultLayer = layer.pipe( Layer.provide(Catalog.defaultLayer), + Layer.provide(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer), - Layer.provide(Layer.orDie(AuthV2.defaultLayer)), + Layer.provide(AccountV2.defaultLayer), + Layer.provide(AgentV2.defaultLayer), + Layer.provide(Config.defaultLayer), Layer.provide(Npm.defaultLayer), ) diff --git a/packages/core/src/plugin/env.ts b/packages/core/src/plugin/env.ts index d63936fa13d..35e6981a40c 100644 --- a/packages/core/src/plugin/env.ts +++ b/packages/core/src/plugin/env.ts @@ -5,12 +5,16 @@ export const EnvPlugin = PluginV2.define({ id: PluginV2.ID.make("env"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - const key = evt.provider.env.find((item) => process.env[item]) - if (!key) return - evt.provider.enabled = { - via: "env", - name: key, + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + const key = item.provider.env.find((env) => process.env[env]) + if (!key) continue + evt.provider.update(item.provider.id, (provider) => { + provider.enabled = { + via: "env", + name: key, + } + }) } }), } diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index e67c2e75a98..4733833fdbf 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,7 +1,8 @@ -import { DateTime, Effect } from "effect" +import { DateTime, Effect, Scope, Stream } from "effect" import { Catalog } from "../catalog" +import { EventV2 } from "../event" import { ModelV2 } from "../model" -import { ModelsDev } from "../models" +import { ModelsDev } from "../models-dev" import { PluginV2 } from "../plugin" import { ProviderV2 } from "../provider" @@ -54,55 +55,66 @@ export const ModelsDevPlugin = PluginV2.define({ effect: Effect.gen(function* () { const catalog = yield* Catalog.Service const modelsDev = yield* ModelsDev.Service - for (const item of Object.values(yield* modelsDev.get())) { - const providerID = ProviderV2.ID.make(item.id) - yield* catalog.provider.update(providerID, (provider) => { - provider.name = item.name - provider.env = [...item.env] - provider.endpoint = item.npm - ? { - type: "aisdk", - package: item.npm, - url: item.api, - } - : { - type: "unknown", - } - }) - - for (const model of Object.values(item.models)) { - const modelID = ModelV2.ID.make(model.id) - yield* catalog.model - .update(providerID, modelID, (draft) => { - draft.name = model.name - draft.family = model.family ? ModelV2.Family.make(model.family) : undefined - draft.endpoint = model.provider?.npm + const events = yield* EventV2.Service + const scope = yield* Scope.Scope + const transform = yield* catalog.transform() + const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () { + const data = yield* modelsDev.get() + yield* transform((catalog) => { + for (const item of Object.values(data)) { + const providerID = ProviderV2.ID.make(item.id) + catalog.provider.update(providerID, (provider) => { + provider.name = item.name + provider.env = [...item.env] + provider.endpoint = item.npm ? { type: "aisdk", - package: model.provider?.npm, - url: model.provider.api, + package: item.npm, + url: item.api, } : { type: "unknown", } - draft.capabilities = { - tools: model.tool_call, - input: [...(model.modalities?.input ?? [])], - output: [...(model.modalities?.output ?? [])], - } - draft.variants = variants(model) - draft.time.released = released(model.release_date) - draft.cost = cost(model.cost) - draft.status = model.status ?? "active" - draft.enabled = true - draft.limit = { - context: model.limit.context, - input: model.limit.input, - output: model.limit.output, - } }) - .pipe(Effect.orDie) - } - } + + for (const model of Object.values(item.models)) { + const modelID = ModelV2.ID.make(model.id) + catalog.model.update(providerID, modelID, (draft) => { + draft.name = model.name + draft.family = model.family ? ModelV2.Family.make(model.family) : undefined + draft.endpoint = model.provider?.npm + ? { + type: "aisdk", + package: model.provider?.npm, + url: model.provider.api, + } + : { + type: "unknown", + } + draft.capabilities = { + tools: model.tool_call, + input: [...(model.modalities?.input ?? [])], + output: [...(model.modalities?.output ?? [])], + } + draft.variants = variants(model) + draft.time.released = released(model.release_date) + draft.cost = cost(model.cost) + draft.status = model.status ?? "active" + draft.enabled = true + draft.limit = { + context: model.limit.context, + input: model.limit.input, + output: model.limit.output, + } + }) + } + } + }) + }) + yield* refresh() + yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( + Stream.runForEach(() => refresh()), + Effect.forkIn(scope, { startImmediately: true }), + ) }).pipe(Effect.provide(ModelsDev.defaultLayer)), }) diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index 366548a0a32..e7452ac2e97 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -50,14 +50,19 @@ export const AmazonBedrockPlugin = PluginV2.define({ id: PluginV2.ID.make("amazon-bedrock"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.amazonBedrock) return - if (evt.provider.endpoint.type !== "aisdk") return - if (typeof evt.provider.options.aisdk.provider.endpoint !== "string") return - // The AI SDK expects a base URL, but users configure Bedrock private/VPC - // endpoints as `endpoint`; move it into the catalog endpoint URL once. - evt.provider.endpoint.url = evt.provider.options.aisdk.provider.endpoint - delete evt.provider.options.aisdk.provider.endpoint + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/amazon-bedrock") continue + evt.provider.update(item.provider.id, (provider) => { + if (provider.endpoint.type !== "aisdk") return + if (typeof provider.options.aisdk.provider.endpoint !== "string") return + // The AI SDK expects a base URL, but users configure Bedrock private/VPC + // endpoints as `endpoint`; move it into the catalog endpoint URL once. + provider.endpoint.url = provider.options.aisdk.provider.endpoint + delete provider.options.aisdk.provider.endpoint + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/amazon-bedrock") return diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index 14851c4a319..026da363492 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -1,15 +1,19 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" export const AnthropicPlugin = PluginV2.define({ id: PluginV2.ID.make("anthropic"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.anthropic) return - evt.provider.options.headers["anthropic-beta"] = - "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/anthropic") continue + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["anthropic-beta"] = + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/anthropic") return diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 6c29a161034..bea98cf2119 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -14,12 +14,18 @@ export const AzurePlugin = PluginV2.define({ id: PluginV2.ID.make("azure"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.azure) return - const configured = evt.provider.options.aisdk.provider.resourceName - const resourceName = - typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME - if (resourceName) evt.provider.options.aisdk.provider.resourceName = resourceName + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/azure") continue + const configured = item.provider.options.aisdk.provider.resourceName + const resourceName = + typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME + if (!resourceName) continue + evt.provider.update(item.provider.id, (provider) => { + provider.options.aisdk.provider.resourceName = resourceName + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/azure") return @@ -49,11 +55,17 @@ export const AzureCognitiveServicesPlugin = PluginV2.define({ id: PluginV2.ID.make("azure-cognitive-services"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("azure-cognitive-services")) return + "catalog.transform": Effect.fn(function* (evt) { const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME - if (resourceName) - evt.provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` + if (!resourceName) return + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (!item.provider.id.includes("azure-cognitive-services")) continue + evt.provider.update(item.provider.id, (provider) => { + provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` + }) + } }), "aisdk.language": Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index b2fadd8bf11..b18884cb6ed 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -1,14 +1,18 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" export const CerebrasPlugin = PluginV2.define({ id: PluginV2.ID.make("cerebras"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("cerebras")) return - evt.provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" + "catalog.transform": Effect.fn(function* (ctx) { + for (const item of ctx.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/cerebras") continue + ctx.provider.update(item.provider.id, (provider) => { + provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/cerebras") return diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index ffcd4adcf46..1cd974ee953 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -45,7 +45,7 @@ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) function gatewayConfig(options: Record): GatewayConfig | undefined { const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") - // AuthPlugin copies CLI prompt metadata into options. The prompt stores the + // AccountPlugin copies CLI prompt metadata into options. The prompt stores the // gateway as gatewayId, while older config examples may use gateway. const gatewayId = process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway") diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index f39869b57d7..32cfb059f43 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -10,13 +10,15 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({ id: PluginV2.ID.make("cloudflare-workers-ai"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== providerID) return - if (evt.provider.endpoint.type !== "aisdk") return - if (evt.provider.endpoint.url) return - - const accountId = resolveAccountId(evt.provider.options.aisdk.provider) - if (accountId) evt.provider.endpoint.url = workersEndpoint(accountId) + "catalog.transform": Effect.fn(function* (evt) { + const item = evt.provider.get(providerID) + if (!item) return + evt.provider.update(item.provider.id, (provider) => { + if (provider.endpoint.type !== "aisdk") return + if (provider.endpoint.url) return + const accountId = resolveAccountId(provider.options.aisdk.provider) + if (accountId) provider.endpoint.url = workersEndpoint(accountId) + }) }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 31e57ba12ab..20b1ad6d6c3 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -15,9 +15,6 @@ export const GithubCopilotPlugin = PluginV2.define({ id: PluginV2.ID.make("github-copilot"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.githubCopilot) return - }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/github-copilot") return const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) @@ -33,11 +30,14 @@ export const GithubCopilotPlugin = PluginV2.define({ ? evt.sdk.responses(evt.model.apiID) : evt.sdk.chat(evt.model.apiID) }), - "model.update": Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return - // This chat-only alias conflicts with the Copilot GPT-5 Responses route, - // so hide it only for Copilot rather than for every provider catalog. - if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true + "catalog.transform": Effect.fn(function* (evt) { + const item = evt.provider.get(ProviderV2.ID.githubCopilot) + if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return + evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + // This chat-only alias conflicts with the Copilot GPT-5 Responses route, + // so hide it only for Copilot rather than for every provider catalog. + model.enabled = false + }) }), } }), diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index 0c335df9312..ae1692b933c 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -25,7 +25,8 @@ function resolveLocation(options: Record) { } function vertexEndpoint(location: string) { - return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` + if (location === "global") return "aiplatform.googleapis.com" + return `${location}-aiplatform.googleapis.com` } function replaceVertexVars(value: string, project: string | undefined, location: string) { @@ -42,9 +43,9 @@ function authFetch(fetchWithRuntimeOptions?: unknown) { // do not, so inject a Google access token into their fetch path. return async (input: Parameters[0], init?: RequestInit) => { const { GoogleAuth } = await import("google-auth-library") - const auth = new GoogleAuth() - const client = await auth.getApplicationDefault() - const token = await client.credential.getAccessToken() + const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }) + const client = await auth.getClient() + const token = await client.getAccessToken() const headers = new Headers(init?.headers) headers.set("Authorization", `Bearer ${token.token}`) return typeof fetchWithRuntimeOptions === "function" @@ -57,20 +58,26 @@ export const GoogleVertexPlugin = PluginV2.define({ id: PluginV2.ID.make("google-vertex"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.googleVertex) return - const project = resolveProject(evt.provider.options.aisdk.provider) - const location = String(resolveLocation(evt.provider.options.aisdk.provider)) - if (project) evt.provider.options.aisdk.provider.project = project - evt.provider.options.aisdk.provider.location = location - if (evt.provider.endpoint.type === "aisdk" && evt.provider.endpoint.url) { - evt.provider.endpoint.url = replaceVertexVars(evt.provider.endpoint.url, project, location) - } - if ( - evt.provider.endpoint.type === "aisdk" && - evt.provider.endpoint.package.includes("@ai-sdk/openai-compatible") - ) { - evt.provider.options.aisdk.provider.fetch = authFetch(evt.provider.options.aisdk.provider.fetch) + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if ( + item.provider.endpoint.package !== "@ai-sdk/google-vertex" && + !item.provider.endpoint.package.includes("@ai-sdk/openai-compatible") + ) + continue + const project = resolveProject(item.provider.options.aisdk.provider) + const location = String(resolveLocation(item.provider.options.aisdk.provider)) + evt.provider.update(item.provider.id, (provider) => { + if (project) provider.options.aisdk.provider.project = project + provider.options.aisdk.provider.location = location + if (provider.endpoint.type === "aisdk" && provider.endpoint.url) { + provider.endpoint.url = replaceVertexVars(provider.endpoint.url, project, location) + } + if (provider.endpoint.type === "aisdk" && provider.endpoint.package.includes("@ai-sdk/openai-compatible")) { + provider.options.aisdk.provider.fetch = authFetch(provider.options.aisdk.provider.fetch) + } + }) } }), "aisdk.sdk": Effect.fn(function* (evt) { @@ -102,34 +109,48 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ id: PluginV2.ID.make("google-vertex-anthropic"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("google-vertex-anthropic")) return - const project = - evt.provider.options.aisdk.provider.project ?? - process.env.GOOGLE_CLOUD_PROJECT ?? - process.env.GCP_PROJECT ?? - process.env.GCLOUD_PROJECT - const location = - evt.provider.options.aisdk.provider.location ?? - process.env.GOOGLE_CLOUD_LOCATION ?? - process.env.VERTEX_LOCATION ?? - "global" - if (project) evt.provider.options.aisdk.provider.project = project - evt.provider.options.aisdk.provider.location = location + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/google-vertex/anthropic") continue + const project = + item.provider.options.aisdk.provider.project ?? + process.env.GOOGLE_CLOUD_PROJECT ?? + process.env.GCP_PROJECT ?? + process.env.GCLOUD_PROJECT + const location = + item.provider.options.aisdk.provider.location ?? + process.env.GOOGLE_CLOUD_LOCATION ?? + process.env.VERTEX_LOCATION ?? + "global" + evt.provider.update(item.provider.id, (provider) => { + if (project) provider.options.aisdk.provider.project = project + provider.options.aisdk.provider.location = location + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google-vertex/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) + const project = + typeof evt.options.project === "string" + ? evt.options.project + : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT) + const location = + typeof evt.options.location === "string" + ? evt.options.location + : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global") evt.sdk = mod.createVertexAnthropic({ ...evt.options, - project: - typeof evt.options.project === "string" - ? evt.options.project - : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT), - location: - typeof evt.options.location === "string" - ? evt.options.location - : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"), + project, + location, + // Continental multi-regions (eu, us) require Regional Endpoint Platform + // domains; the default {region}-aiplatform.googleapis.com does not resolve. + ...((location === "eu" || location === "us") && project && !evt.options.baseURL + ? { + baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models`, + } + : {}), }) }), "aisdk.language": Effect.fn(function* (evt) { diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index 82eca003d10..60b0482537a 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -1,37 +1,45 @@ -// kilocode_change - new file -import { createKilo, KILO_OPENROUTER_BASE } from "@kilocode/kilo-gateway" +import { createKilo, KILO_OPENROUTER_BASE } from "@kilocode/kilo-gateway" // kilocode_change import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change -const id = ProviderV2.ID.make("kilo") +const id = ProviderV2.ID.make("kilo") // kilocode_change export const KiloPlugin = PluginV2.define({ id: PluginV2.ID.make("kilo"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== id) return + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.id !== id) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + // kilocode_change start + const options = provider.options.aisdk.provider + const token = options.kilocodeToken ?? options.apiKey ?? process.env.KILO_API_KEY + const org = process.env.KILO_ORG_ID ?? options.kilocodeOrganizationId - const options = evt.provider.options.aisdk.provider - const token = options.kilocodeToken ?? options.apiKey ?? process.env.KILO_API_KEY - const org = process.env.KILO_ORG_ID ?? options.kilocodeOrganizationId - - evt.provider.endpoint = { - type: "aisdk", - package: "@kilocode/kilo-gateway", - url: KILO_OPENROUTER_BASE, + provider.endpoint = { + type: "aisdk", + package: "@kilocode/kilo-gateway", + url: KILO_OPENROUTER_BASE, + } + // kilocode_change end + provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" + // kilocode_change start + provider.options.headers["X-Title"] = "Kilo Code" + options.kilocodeToken = token ?? "anonymous" + if (org) options.kilocodeOrganizationId = org + if (!provider.enabled) provider.enabled = { via: "custom", data: { anonymous: true } } + // kilocode_change end + }) } - evt.provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" - evt.provider.options.headers["X-Title"] = "Kilo Code" - options.kilocodeToken = token ?? "anonymous" - if (org) options.kilocodeOrganizationId = org - if (!evt.provider.enabled) evt.provider.enabled = { via: "custom", data: { anonymous: true } } }), + // kilocode_change start "aisdk.sdk": Effect.fn(function* (evt) { if (evt.model.providerID !== id) return evt.sdk = createKilo(evt.options) }), + // kilocode_change end } }), }) diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index dd656dfc59d..818a30b971d 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -1,17 +1,26 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change export const LLMGatewayPlugin = PluginV2.define({ id: PluginV2.ID.make("llmgateway"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("llmgateway")) return - if (evt.provider.enabled === false) return - evt.provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change - evt.provider.options.headers["X-Title"] = "Kilo Code" // kilocode_change - evt.provider.options.headers["X-Source"] = "kilo" // kilocode_change + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.enabled === false) continue + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.endpoint.url !== "https://api.llmgateway.io/v1") continue + if (item.provider.id !== ProviderV2.ID.make("llmgateway")) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + // kilocode_change start + provider.options.headers["X-Title"] = "Kilo Code" + provider.options.headers["X-Source"] = "kilo" + // kilocode_change end + }) + } }), } }), diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index ebbca16d2af..1cd0e40acc7 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -1,16 +1,25 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change export const NvidiaPlugin = PluginV2.define({ id: PluginV2.ID.make("nvidia"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("nvidia")) return - evt.provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change - evt.provider.options.headers["X-Title"] = "Kilo Code" // kilocode_change - evt.provider.options.headers["X-BILLING-INVOKE-ORIGIN"] ??= "KiloCode" // kilocode_change + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.endpoint.url !== "https://integrate.api.nvidia.com/v1") continue + if (item.provider.id !== ProviderV2.ID.make("nvidia")) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + // kilocode_change start + provider.options.headers["X-Title"] = "Kilo Code" + provider.options.headers["X-BILLING-INVOKE-ORIGIN"] ??= "KiloCode" + // kilocode_change end + }) + } }), } }), diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index a81455f1985..2d33fbcbbe1 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -16,11 +16,17 @@ export const OpenAIPlugin = PluginV2.define({ if (evt.model.providerID !== ProviderV2.ID.openai) return evt.language = evt.sdk.responses(evt.model.apiID) }), - "model.update": Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.openai) return - // OpenAIPlugin sends OpenAI models through Responses; this alias is a - // chat-completions-only model, so remove it only from OpenAI's catalog. - if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/openai") continue + if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue + evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + // OpenAIPlugin sends OpenAI models through Responses; this alias is a + // chat-completions-only model, so hide it only from OpenAI's catalog. + model.enabled = false + }) + } }), } }), diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 10bbb62dadb..64d20f8bd4e 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -7,20 +7,25 @@ export const OpencodePlugin = PluginV2.define({ effect: Effect.gen(function* () { let hasKey = false return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.opencode) return + "catalog.transform": Effect.fn(function* (evt) { + const item = evt.provider.get(ProviderV2.ID.opencode) + if (!item) return hasKey = Boolean( process.env.OPENCODE_API_KEY || - evt.provider.env.some((item) => process.env[item]) || - evt.provider.options.aisdk.provider.apiKey || - (evt.provider.enabled && evt.provider.enabled.via === "auth"), + item.provider.env.some((env) => process.env[env]) || + item.provider.options.aisdk.provider.apiKey || + (item.provider.enabled && item.provider.enabled.via === "account"), ) - if (!hasKey) evt.provider.options.aisdk.provider.apiKey = "public" - }), - "model.update": Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.opencode) return + evt.provider.update(item.provider.id, (provider) => { + if (!hasKey) provider.options.aisdk.provider.apiKey = "public" + }) if (hasKey) return - if (evt.model.cost.some((item) => item.input > 0)) evt.cancel = true + for (const model of item.models.values()) { + if (!model.cost.some((cost) => cost.input > 0)) continue + evt.model.update(item.provider.id, model.id, (draft) => { + draft.enabled = false + }) + } }), } }), diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index b3e926137f7..bbee2d62341 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -1,29 +1,36 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change export const OpenRouterPlugin = PluginV2.define({ id: PluginV2.ID.make("openrouter"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.openrouter) return - evt.provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change - evt.provider.options.headers["X-Title"] = "Kilo Code" // kilocode_change + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@openrouter/ai-sdk-provider") continue + if (item.provider.id !== ProviderV2.ID.openrouter) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["HTTP-Referer"] = "https://kilo.ai/" // kilocode_change + provider.options.headers["X-Title"] = "Kilo Code" // kilocode_change + }) + for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { + if (!item.models.has(modelID)) continue + evt.model.update(item.provider.id, modelID, (model) => { + // These are OpenRouter-specific OpenAI chat aliases that do not work + // on the generic path. Keep custom providers with matching IDs untouched. + model.enabled = false + }) + } + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@openrouter/ai-sdk-provider") return const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) evt.sdk = mod.createOpenRouter(evt.options) }), - "model.update": Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.openrouter) return - // These are OpenRouter-specific OpenAI chat aliases that do not work on - // the generic path. Keep custom providers with matching IDs untouched. - if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true - if (evt.model.id === ModelV2.ID.make("openai/gpt-5-chat")) evt.cancel = true - }), } }), }) diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index 2d50e3b0930..961bbd2ea5d 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -1,15 +1,21 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change export const VercelPlugin = PluginV2.define({ id: PluginV2.ID.make("vercel"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("vercel")) return - evt.provider.options.headers["http-referer"] = "https://kilo.ai/" // kilocode_change - evt.provider.options.headers["x-title"] = "Kilo Code" // kilocode_change + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/vercel") continue + if (item.provider.id !== ProviderV2.ID.make("vercel")) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["http-referer"] = "https://kilo.ai/" // kilocode_change + provider.options.headers["x-title"] = "Kilo Code" // kilocode_change + }) + } }), "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/vercel") return diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index be8086f4826..2433b4385f8 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -1,15 +1,22 @@ import { Effect } from "effect" import { PluginV2 } from "../../plugin" -import { ProviderV2 } from "../../provider" +import { ProviderV2 } from "../../provider" // kilocode_change export const ZenmuxPlugin = PluginV2.define({ id: PluginV2.ID.make("zenmux"), effect: Effect.gen(function* () { return { - "provider.update": Effect.fn(function* (evt) { - if (evt.provider.id !== ProviderV2.ID.make("zenmux")) return - evt.provider.options.headers["HTTP-Referer"] ??= "https://kilo.ai/" // kilocode_change - evt.provider.options.headers["X-Title"] ??= "Kilo Code" // kilocode_change + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.endpoint.type !== "aisdk") continue + if (item.provider.endpoint.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.endpoint.url !== "https://zenmux.ai/api/v1") continue + if (item.provider.id !== ProviderV2.ID.make("zenmux")) continue // kilocode_change + evt.provider.update(item.provider.id, (provider) => { + provider.options.headers["HTTP-Referer"] ??= "https://kilo.ai/" // kilocode_change + provider.options.headers["X-Title"] ??= "Kilo Code" // kilocode_change + }) + } }), } }), diff --git a/packages/core/src/policy.ts b/packages/core/src/policy.ts new file mode 100644 index 00000000000..78bd74f1cd4 --- /dev/null +++ b/packages/core/src/policy.ts @@ -0,0 +1,44 @@ +export * as Policy from "./policy" + +import { Context, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Wildcard } from "./util/wildcard" +import { Location } from "./location" + +export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" }) +export type Effect = typeof Effect.Type + +export class Info extends Schema.Class("Policy.Info")({ + action: Schema.String, + effect: Effect, + resource: Schema.String, +}) {} + +export interface Interface { + readonly load: (statements: Info[]) => EffectRuntime.Effect + readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Policy") {} + +export const layer = Layer.effect( + Service, + EffectRuntime.gen(function* () { + let statements: Info[] = [] + yield* Location.Service + + return Service.of({ + load: EffectRuntime.fn("Policy.load")(function* (input) { + statements = input + }), + evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) { + return ( + statements.findLast( + (statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource), + )?.effect ?? fallback + ) + }), + }) + }), +) + +export const defaultLayer = layer diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts new file mode 100644 index 00000000000..63f075a605e --- /dev/null +++ b/packages/core/src/project.ts @@ -0,0 +1,128 @@ +export * as Project from "./project" + +import { Context, Effect, Layer, Schema } from "effect" +import path from "path" +import { AbsolutePath, withStatics } from "./schema" +import { AppFileSystem } from "./filesystem" +import { Git } from "./git" +import { Hash } from "./util/hash" + +export const ID = Schema.String.pipe( + Schema.brand("Project.ID"), + withStatics((schema) => ({ + global: schema.make("global"), + })), +) +export type ID = typeof ID.Type + +export const Vcs = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("git"), + store: AbsolutePath, + }), +]) +export type Vcs = typeof Vcs.Type + +export class Info extends Schema.Class("Project.Info")({ + id: ID, +}) {} + +export interface Interface { + readonly resolve: (input: AbsolutePath) => Effect.Effect< + { + previous?: ID + id: ID + directory: AbsolutePath + vcs?: Vcs + }, + never + > + /** + * Temporary bridge method for writing the resolved project ID to the repo-local cache. + * + * This exists while the old opencode project service and this core project + * service work together: core resolves the ID, while the old service still owns + * database migration and persistence. The old service should call this after it + * finishes migrating from `resolve().previous` to `resolve().id`; once project + * persistence moves into core, this separate bridge method can go away. + */ + readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectV2") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const git = yield* Git.Service + + const cached = Effect.fnUntraced(function* (dir: string) { + return yield* fs.readFileString(path.join(dir, "kilo")).pipe( // kilocode_change + Effect.map((value) => value.trim()), + Effect.map((value) => (value ? ID.make(value) : undefined)), + Effect.catch(() => Effect.succeed(undefined)), + ) + }) + + const remote = Effect.fnUntraced(function* (repo: Git.Repo) { + const origin = yield* git.remote(repo) + if (!origin) return undefined + const normalized = url(origin) + if (!normalized) return undefined + return ID.make(Hash.fast(`git-remote:${normalized}`)) + }) + + function url(input: string) { + const value = input.trim() + if (!value) return undefined + + try { + const parsed = new URL(value) + if (parsed.protocol === "file:") return undefined + return parts(parsed.hostname, parsed.pathname) + } catch { + const scp = value.match(/^([^@/:]+@)?([^/:]+):(.+)$/) + if (scp) return parts(scp[2], scp[3]) + return undefined + } + } + + function parts(host: string, name: string) { + const pathname = name + .replace(/^\/+/, "") + .replace(/\.git\/?$/, "") + .replace(/\/+$/, "") + if (!host || !pathname) return undefined + return `${host.toLowerCase()}/${pathname}` + } + + const root = Effect.fnUntraced(function* (repo: Git.Repo) { + const root = (yield* git.roots(repo))[0] + return root ? ID.make(root) : undefined + }) + + const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) { + const repo = yield* git.find(input) + if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined } + + const previous = yield* cached(repo.store) + const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo)) + + return { + previous, + id: id ?? ID.global, + directory: repo.directory, + vcs: { type: "git" as const, store: repo.store }, + } + }) + + const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) { + yield* fs.writeFileString(path.join(input.store, "kilo"), input.id).pipe(Effect.ignore) // kilocode_change + }) + + return Service.of({ resolve, commit }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer)) diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 7c1c9666542..7ba2172ada3 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -86,7 +86,7 @@ export class Info extends Schema.Class("ProviderV2.Info")({ name: Schema.String, }), Schema.Struct({ - via: Schema.Literal("auth"), + via: Schema.Literal("account"), service: Schema.String, }), Schema.Struct({ diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 5b4042c7369..523a4eace5d 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,5 +1,11 @@ import { Option, Schema, SchemaGetter } from "effect" +export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) +export type AbsolutePath = typeof AbsolutePath.Type + +export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) +export type RelativePath = typeof RelativePath.Type + /** * Integer greater than zero. */ diff --git a/packages/core/src/state.ts b/packages/core/src/state.ts new file mode 100644 index 00000000000..b764699e08d --- /dev/null +++ b/packages/core/src/state.ts @@ -0,0 +1,63 @@ +export * as State from "./state" + +import { Effect, Scope, Semaphore } from "effect" +import { createDraft, finishDraft, type Draft, type Objectish } from "immer" + +export type Transform = (editor: Editor) => void +export type MakeEditor = (draft: Draft) => Editor + +export interface Options { + readonly initial: () => State + readonly editor: MakeEditor + /** Completes every committed edit; reason identifies exceptional update origins. */ + readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect +} + +export interface Interface { + readonly get: () => State + readonly transform: () => Effect.Effect<(transform: Transform) => Effect.Effect, never, Scope.Scope> + readonly update: (update: (editor: Editor) => Effect.Effect, reason?: string) => Effect.Effect +} + +export function create(options: Options): Interface { + let state = options.initial() + let transforms: { update: Transform }[] = [] + const semaphore = Semaphore.makeUnsafe(1) + + const commit = Effect.fn("State.commit")(function* (draft: Draft, reason?: string) { + const api = options.editor(draft) + if (options.finalize) yield* options.finalize(api, reason) + state = finishDraft(draft) as State + }) + + const rebuild = Effect.fn("State.rebuild")(function* () { + const draft = createDraft(options.initial()) + const api = options.editor(draft) + for (const transform of transforms) transform.update(api) + yield* commit(draft) + }, semaphore.withPermit) + + return { + get: () => state, + transform: Effect.fn("State.transform")(function* () { + const transform = { update: (_editor: Editor) => {} } + transforms = [...transforms, transform] + const scope = yield* Scope.Scope + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + transforms = transforms.filter((item) => item !== transform) + }).pipe(Effect.andThen(rebuild())), + ) + return Effect.fnUntraced(function* (update: Transform) { + transform.update = update + yield* rebuild() + }) + }), + update: Effect.fn("State.update")(function* (update, reason) { + const draft = createDraft(state) + yield* update(options.editor(draft)) + yield* commit(draft, reason) + }, semaphore.withPermit), + } +} diff --git a/packages/core/src/util/error.ts b/packages/core/src/util/error.ts index 7338571f298..5fe41e1cff9 100644 --- a/packages/core/src/util/error.ts +++ b/packages/core/src/util/error.ts @@ -65,5 +65,6 @@ export abstract class NamedError extends Error { public static readonly Unknown = NamedError.create("UnknownError", { message: Schema.String, + ref: Schema.optional(Schema.String), }) } diff --git a/packages/core/src/util/wildcard.ts b/packages/core/src/util/wildcard.ts new file mode 100644 index 00000000000..0a67817bcb5 --- /dev/null +++ b/packages/core/src/util/wildcard.ts @@ -0,0 +1,14 @@ +export * as Wildcard from "./wildcard" + +export function match(input: string, pattern: string) { + const normalized = input.replaceAll("\\", "/") + let escaped = pattern + .replaceAll("\\", "/") + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*") + .replace(/\?/g, ".") + + if (escaped.endsWith(" .*")) escaped = escaped.slice(0, -3) + "( .*)?" + + return new RegExp("^" + escaped + "$", process.platform === "win32" ? "si" : "s").test(normalized) +} diff --git a/packages/core/test/account.test.ts b/packages/core/test/account.test.ts new file mode 100644 index 00000000000..1e50fd340ae --- /dev/null +++ b/packages/core/test/account.test.ts @@ -0,0 +1,287 @@ +import path from "path" +import { describe, expect } from "bun:test" +import { produce } from "immer" +import { Effect, Fiber, Layer, Option, Stream } from "effect" +import { AccountV2 } from "@opencode-ai/core/account" +import { Catalog } from "@opencode-ai/core/catalog" +import { EventV2 } from "@opencode-ai/core/event" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { AccountPlugin } from "@opencode-ai/core/plugin/account" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const it = testEffect(PluginV2.defaultLayer) + +function context( + records: { provider: ProviderV2.Info; models: Map }[], + updates: Array<{ id: ProviderV2.ID; enabled: ProviderV2.Info["enabled"]; apiKey?: string }>, +): Catalog.Editor { + return { + provider: { + list: () => records, + get: (providerID) => records.find((item) => item.provider.id === providerID), + update: (providerID, fn) => { + const record = records.find((item) => item.provider.id === providerID) + const provider = produce(record?.provider ?? ProviderV2.Info.empty(providerID), fn) + if (record) record.provider = provider + else records.push({ provider, models: new Map() }) + updates.push({ + id: providerID, + enabled: provider.enabled, + apiKey: + typeof provider.options.aisdk.provider.apiKey === "string" + ? provider.options.aisdk.provider.apiKey + : undefined, + }) + }, + remove: (providerID) => { + const index = records.findIndex((item) => item.provider.id === providerID) + if (index !== -1) records.splice(index, 1) + }, + }, + model: { + get: () => undefined, + update: () => {}, + remove: () => {}, + default: { + get: () => undefined, + set: () => {}, + }, + }, + } +} + +function testLayer(dir: string) { + return AccountV2.layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provideMerge(EventV2.defaultLayer), + Layer.provide( + Global.layerWith({ + data: dir, + cache: path.join(dir, "cache"), + config: path.join(dir, "config"), + state: path.join(dir, "state"), + tmp: path.join(dir, "tmp"), + bin: path.join(dir, "bin"), + log: path.join(dir, "log"), + repos: path.join(dir, "repos"), + }), + ), + ) +} + +describe("AccountV2", () => { + it.live("emits account lifecycle events", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const accounts = yield* AccountV2.Service + const eventSvc = yield* EventV2.Service + const addedFiber = yield* eventSvc + .subscribe(AccountV2.Event.Added) + .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + const switchedFiber = yield* eventSvc + .subscribe(AccountV2.Event.Switched) + .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) + const removedFiber = yield* eventSvc + .subscribe(AccountV2.Event.Removed) + .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + + yield* Effect.yieldNow + + const first = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "raw-key" }), + }) + expect(first).toBeDefined() + if (!first) return + expect(first.description).toBe("default") + expect(first.credential.type).toBe("api") + if (first.credential.type === "api") expect(first.credential.key).toBe("raw-key") + + yield* accounts.update(first.id, { description: "keep" }) + const updated = yield* accounts.get(first.id) + expect(updated?.description).toBe("keep") + expect(updated?.credential.type).toBe("api") + if (updated?.credential.type === "api") expect(updated.credential.key).toBe("raw-key") + + const second = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "second-key" }), + }) + expect(second).toBeDefined() + if (!second) return + + yield* accounts.remove(second.id) + const added = Array.from(yield* Fiber.join(addedFiber)) + const switched = Array.from(yield* Fiber.join(switchedFiber)) + const removed = Array.from(yield* Fiber.join(removedFiber)) + expect(added.map((event) => event.data.account.id)).toEqual([first.id, second.id]) + expect(switched.map((event) => event.data)).toEqual([ + { serviceID: AccountV2.ServiceID.make("provider"), from: undefined, to: first.id }, + { serviceID: AccountV2.ServiceID.make("provider"), from: first.id, to: second.id }, + { serviceID: AccountV2.ServiceID.make("provider"), from: second.id, to: first.id }, + ]) + expect(removed[0]?.data.account.id).toBe(second.id) + }).pipe(Effect.provide(testLayer(tmp.path))), + ), + ), + ) + + it.live("always switches to newly created accounts", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const accounts = yield* AccountV2.Service + const eventSvc = yield* EventV2.Service + const switchedFiber = yield* eventSvc + .subscribe(AccountV2.Event.Switched) + .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) + + yield* Effect.yieldNow + + const first = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "first-key" }), + }) + const second = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "second-key" }), + }) + const third = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "third-key" }), + }) + + expect(first).toBeDefined() + expect(second).toBeDefined() + expect(third).toBeDefined() + if (!first || !second || !third) return + + expect((yield* accounts.active(AccountV2.ServiceID.make("provider")))?.id).toBe(third.id) + expect(Array.from(yield* Fiber.join(switchedFiber)).map((event) => event.data)).toEqual([ + { serviceID: AccountV2.ServiceID.make("provider"), from: undefined, to: first.id }, + { serviceID: AccountV2.ServiceID.make("provider"), from: first.id, to: second.id }, + { serviceID: AccountV2.ServiceID.make("provider"), from: second.id, to: third.id }, + ]) + }).pipe(Effect.provide(testLayer(tmp.path))), + ), + ), + ) + + it.live("account plugin refreshes providers on account lifecycle events", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const accounts = yield* AccountV2.Service + const plugin = yield* PluginV2.Service + const records = [ + { + provider: ProviderV2.Info.empty(ProviderV2.ID.make("provider")), + models: new Map(), + }, + ] + const updates: Array<{ id: ProviderV2.ID; enabled: ProviderV2.Info["enabled"]; apiKey?: string }> = [] + const catalog = Catalog.Service.of({ + transform: () => Effect.die("unexpected catalog.transform"), + provider: { + get: () => Effect.die("unexpected provider.get"), + all: () => Effect.succeed([]), + available: () => Effect.succeed([]), + }, + model: { + get: () => Effect.die("unexpected model.get"), + all: () => Effect.succeed([]), + available: () => Effect.succeed([]), + default: () => Effect.succeed(Option.none()), + small: () => Effect.succeed(Option.none()), + }, + }) + + const eventSvc = yield* EventV2.Service + yield* plugin.add({ + ...AccountPlugin, + effect: AccountPlugin.effect.pipe( + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(EventV2.Service, eventSvc), + Effect.provideService(PluginV2.Service, plugin), + ), + }) + yield* Effect.yieldNow + + const first = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "first-key" }), + }) + expect(first).toBeDefined() + if (!first) return + yield* plugin.trigger("catalog.transform", context(records, updates), {}) + expect(updates).toEqual([ + { + id: ProviderV2.ID.make("provider"), + enabled: { via: "account", service: AccountV2.ServiceID.make("provider") }, + apiKey: "first-key", + }, + ]) + + updates.length = 0 + const second = yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("provider"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "second-key" }), + }) + expect(second).toBeDefined() + if (!second) return + yield* plugin.trigger("catalog.transform", context(records, updates), {}) + expect(updates).toEqual([ + { + id: ProviderV2.ID.make("provider"), + enabled: { via: "account", service: AccountV2.ServiceID.make("provider") }, + apiKey: "second-key", + }, + ]) + + updates.length = 0 + yield* accounts.activate(first.id) + yield* plugin.trigger("catalog.transform", context(records, updates), {}) + expect(updates).toEqual([ + { + id: ProviderV2.ID.make("provider"), + enabled: { via: "account", service: AccountV2.ServiceID.make("provider") }, + apiKey: "first-key", + }, + ]) + + updates.length = 0 + yield* accounts.remove(first.id) + yield* plugin.trigger("catalog.transform", context(records, updates), {}) + expect(updates).toEqual([ + { + id: ProviderV2.ID.make("provider"), + enabled: { via: "account", service: AccountV2.ServiceID.make("provider") }, + apiKey: "second-key", + }, + ]) + + updates.length = 0 + yield* accounts.remove(second.id) + yield* plugin.trigger("catalog.transform", context(records, updates), {}) + expect(updates).toEqual([]) + }).pipe(Effect.provide(testLayer(tmp.path))), + ), + ), + ) +}) diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts new file mode 100644 index 00000000000..c59449e9388 --- /dev/null +++ b/packages/core/test/agent.test.ts @@ -0,0 +1,101 @@ +import { describe, expect } from "bun:test" +import { Effect, Exit, Scope } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { testEffect } from "./lib/effect" + +const it = testEffect(AgentV2.defaultLayer) + +describe("AgentV2", () => { + it.effect("starts without agents", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + + expect(yield* agent.all()).toEqual([]) + expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined() + }), + ) + + it.effect("materializes replayable agent transforms", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + const id = AgentV2.ID.make("reviewer") + const transform = yield* agent.transform() + + yield* transform((editor) => + editor.update(id, (info) => { + info.description = "Reviews code" + info.mode = "subagent" + }), + ) + + expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" }) + expect((yield* agent.all()).map((info) => info.id)).toEqual([id]) + }), + ) + + it.effect("rebuilds state when a transform is replaced", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + const id = AgentV2.ID.make("reviewer") + const transform = yield* agent.transform() + + yield* transform((editor) => + editor.update(id, (info) => { + info.description = "Old description" + info.hidden = true + }), + ) + yield* transform((editor) => + editor.update(id, (info) => { + info.description = "New description" + }), + ) + + expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false }) + }), + ) + + it.effect("removes a transform contribution when its scope closes", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + const id = AgentV2.ID.make("scoped") + const scope = yield* Scope.make() + const transform = yield* agent.transform().pipe(Scope.provide(scope)) + + yield* transform((editor) => editor.update(id, () => {})) + expect(yield* agent.get(id)).toBeDefined() + + yield* Scope.close(scope, Exit.void) + expect(yield* agent.get(id)).toBeUndefined() + }), + ) + + it.effect("applies direct agent updates", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + const id = AgentV2.ID.make("build") + + yield* agent.update((editor) => + editor.update(id, (info) => { + info.mode = "primary" + info.hidden = true + }), + ) + + expect(yield* agent.get(id)).toMatchObject({ id, mode: "primary", hidden: true }) + }), + ) + + it.effect("creates agents with runtime defaults and supports direct removal", () => + Effect.gen(function* () { + const agent = yield* AgentV2.Service + const id = AgentV2.ID.make("custom") + + yield* agent.update((editor) => editor.update(id, () => {})) + expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id)) + + yield* agent.update((editor) => editor.remove(id)) + expect(yield* agent.get(id)).toBeUndefined() + }), + ) +}) diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 594f42d1c8b..9736e2a7393 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -1,18 +1,25 @@ import { describe, expect } from "bun:test" -import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect" +import { DateTime, Effect, Layer, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" +import { Policy } from "@opencode-ai/core/policy" import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" import { testEffect } from "./lib/effect" -const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" })) +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make("test") })), +) const it = testEffect( Catalog.layer.pipe( Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(PluginV2.defaultLayer), + Layer.provideMerge(Policy.defaultLayer), Layer.provideMerge(locationLayer), ), ) @@ -22,24 +29,24 @@ describe("CatalogV2", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, (provider) => { - provider.endpoint = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://default.example.com", - } - provider.options.aisdk.provider.baseURL = "https://override.example.com" - }) + yield* transform((catalog) => + catalog.provider.update(providerID, (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://default.example.com", + } + provider.options.aisdk.provider.baseURL = "https://override.example.com" + }), + ) - const provider = yield* catalog.provider.get(providerID) - - expect(provider.endpoint).toEqual({ + expect((yield* catalog.provider.get(providerID)).endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", }) - expect(provider.options.aisdk.provider.baseURL).toBeUndefined() }), ) @@ -48,56 +55,27 @@ describe("CatalogV2", () => { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, (provider) => { - provider.endpoint = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://provider.example.com", - } - }) - yield* catalog.model.update(providerID, modelID, (model) => { - model.endpoint = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://model.example.com", - } - model.options.aisdk.provider.baseURL = "https://override.example.com" + yield* transform((catalog) => { + catalog.provider.update(providerID, (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://provider.example.com", + } + }) + catalog.model.update(providerID, modelID, (model) => { + model.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://model.example.com" } + model.options.aisdk.provider.baseURL = "https://override.example.com" + }) }) - const model = yield* catalog.model.get(providerID, modelID) - - expect(model.endpoint).toEqual({ + expect((yield* catalog.model.get(providerID, modelID)).endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", }) - expect(model.options.aisdk.provider.baseURL).toBeUndefined() - }), - ) - - it.effect("publishes model updated events", () => - Effect.gen(function* () { - const catalog = yield* Catalog.Service - const events = yield* EventV2.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") - const fiber = yield* events - .subscribe(Catalog.Event.ModelUpdated) - .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) - - yield* Effect.yieldNow - yield* catalog.provider.update(providerID, () => {}) - yield* catalog.model.update(providerID, modelID, (model) => { - model.name = "Updated Model" - }) - const event = Array.from(yield* Fiber.join(fiber))[0] - - expect(event?.type).toBe("catalog.model.updated") - expect(event?.data.model.providerID).toBe(providerID) - expect(event?.data.model.id).toBe(modelID) - expect(event?.data.model.name).toBe("Updated Model") - expect(event?.location).toEqual({ directory: "test" }) }), ) @@ -106,19 +84,20 @@ describe("CatalogV2", () => { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, (provider) => { - provider.endpoint = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://provider.example.com", - } + yield* transform((catalog) => { + catalog.provider.update(providerID, (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://provider.example.com", + } + }) + catalog.model.update(providerID, modelID, () => {}) }) - yield* catalog.model.update(providerID, modelID, () => {}) - const model = yield* catalog.model.get(providerID, modelID) - - expect(model.endpoint).toEqual({ + expect((yield* catalog.model.get(providerID, modelID)).endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", @@ -126,58 +105,91 @@ describe("CatalogV2", () => { }), ) - it.effect("runs provider hooks after baseURL is normalized", () => + it.effect("runs catalog transform hooks after baseURL is normalized", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const plugin = yield* PluginV2.Service const providerID = ProviderV2.ID.make("test") const seen: unknown[] = [] + const transform = yield* catalog.transform() yield* plugin.add({ id: PluginV2.ID.make("test"), effect: Effect.succeed({ - "provider.update": (evt) => + "catalog.transform": (evt) => Effect.sync(() => { - seen.push(evt.provider.endpoint.type) - if (evt.provider.endpoint.type === "aisdk") seen.push(evt.provider.endpoint.url) - seen.push(evt.provider.options.aisdk.provider.baseURL) + const item = evt.provider.get(providerID) + if (!item) return + seen.push(item.provider.endpoint.type) + if (item?.provider.endpoint.type === "aisdk") seen.push(item.provider.endpoint.url) + seen.push(item?.provider.options.aisdk.provider.baseURL) }), }), }) - yield* catalog.provider.update(providerID, (provider) => { - provider.endpoint = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - } - provider.options.aisdk.provider.baseURL = "https://provider.example.com" - }) + yield* transform((catalog) => + catalog.provider.update(providerID, (provider) => { + provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" } + provider.options.aisdk.provider.baseURL = "https://provider.example.com" + }), + ) expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined]) }), ) + it.effect("runs catalog transform when a plugin is added", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const plugin = yield* PluginV2.Service + const providerID = ProviderV2.ID.make("test") + const transform = yield* catalog.transform() + + yield* transform((catalog) => + catalog.provider.update(providerID, (provider) => { + provider.name = "Before" + }), + ) + yield* plugin.add({ + id: PluginV2.ID.make("test-transform"), + effect: Effect.succeed({ + "catalog.transform": (evt) => + Effect.sync(() => + evt.provider.update(providerID, (provider) => { + provider.name = "After" + }), + ), + }), + }) + yield* Effect.yieldNow + + expect((yield* catalog.provider.get(providerID)).name).toBe("After") + }), + ) + it.effect("resolves provider and model option merges", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, (provider) => { - provider.options.headers.provider = "provider" - provider.options.headers.shared = "provider" - provider.options.body.provider = true - provider.options.aisdk.provider.provider = true - }) - yield* catalog.model.update(providerID, modelID, (model) => { - model.options.headers.model = "model" - model.options.headers.shared = "model" - model.options.body.model = true - model.options.aisdk.provider.model = true - model.options.aisdk.request.request = true + yield* transform((catalog) => { + catalog.provider.update(providerID, (provider) => { + provider.options.headers.provider = "provider" + provider.options.headers.shared = "provider" + provider.options.body.provider = true + provider.options.aisdk.provider.provider = true + }) + catalog.model.update(providerID, modelID, (model) => { + model.options.headers.model = "model" + model.options.headers.shared = "model" + model.options.body.model = true + model.options.aisdk.provider.model = true + model.options.aisdk.request.request = true + }) }) const model = yield* catalog.model.get(providerID, modelID) - expect(model.options.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) expect(model.options.body).toEqual({ provider: true, model: true }) expect(model.options.aisdk.provider).toEqual({ provider: true, model: true }) @@ -189,20 +201,52 @@ describe("CatalogV2", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, (provider) => { - provider.enabled = { via: "custom", data: {} } - }) - yield* catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { - model.time.released = DateTime.makeUnsafe(1000) - }) - yield* catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { - model.time.released = DateTime.makeUnsafe(2000) + yield* transform((catalog) => { + catalog.provider.update(providerID, (provider) => { + provider.enabled = { via: "custom", data: {} } + }) + catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { + model.time.released = DateTime.makeUnsafe(1000) + }) + catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { + model.time.released = DateTime.makeUnsafe(2000) + }) }) - const model = yield* catalog.model.default() + expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new") + }), + ) - expect(Option.getOrUndefined(model)?.id).toMatch("new") + it.effect("uses a transform-provided default model until that transform is replaced", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("test") + const old = ModelV2.ID.make("old") + const newest = ModelV2.ID.make("new") + const transform = yield* catalog.transform() + + const models = (catalog: Catalog.Editor) => { + catalog.provider.update(providerID, (provider) => { + provider.enabled = { via: "custom", data: {} } + }) + catalog.model.update(providerID, old, (model) => { + model.time.released = DateTime.makeUnsafe(1000) + }) + catalog.model.update(providerID, newest, (model) => { + model.time.released = DateTime.makeUnsafe(2000) + }) + } + + yield* transform((catalog) => { + models(catalog) + catalog.model.default.set(providerID, old) + }) + expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old) + + yield* transform(models) + expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest) }), ) @@ -210,24 +254,44 @@ describe("CatalogV2", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") + const transform = yield* catalog.transform() - yield* catalog.provider.update(providerID, () => {}) - yield* catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { - model.capabilities.input = ["text"] - model.capabilities.output = ["text"] - model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }] - model.time.released = DateTime.makeUnsafe(Date.now()) - }) - yield* catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => { - model.capabilities.input = ["text"] - model.capabilities.output = ["text"] - model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }] - model.time.released = DateTime.makeUnsafe(Date.now()) + yield* transform((catalog) => { + catalog.provider.update(providerID, () => {}) + catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { + model.capabilities.input = ["text"] + model.capabilities.output = ["text"] + model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }] + model.time.released = DateTime.makeUnsafe(Date.now()) + }) + catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => { + model.capabilities.input = ["text"] + model.capabilities.output = ["text"] + model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }] + model.time.released = DateTime.makeUnsafe(Date.now()) + }) }) - const model = yield* catalog.model.small(providerID) + expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini") + }), + ) - expect(Option.getOrUndefined(model)?.id).toMatch("expensive-mini") + it.effect("removes providers denied by policy after loading", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const policy = yield* Policy.Service + const providerID = ProviderV2.ID.make("blocked") + const transform = yield* catalog.transform() + + yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })]) + yield* transform((catalog) => { + catalog.provider.update(providerID, () => {}) + catalog.model.update(providerID, ModelV2.ID.make("model"), () => {}) + }) + + expect(yield* catalog.provider.all()).toEqual([]) + expect(yield* catalog.model.all()).toEqual([]) + expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none()) }), ) }) diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts new file mode 100644 index 00000000000..0d8562313be --- /dev/null +++ b/packages/core/test/config/agent.test.ts @@ -0,0 +1,186 @@ +import { describe, expect } from "bun:test" +import { Effect, Schema } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { Config } from "@opencode-ai/core/config" +import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { testEffect } from "../lib/effect" + +const it = testEffect(AgentV2.defaultLayer) +const decode = Schema.decodeUnknownSync(Config.Info) + +describe("ConfigAgentPlugin.Plugin", () => { + it.effect("applies global permissions between built-in and agent-specific permissions", () => + Effect.gen(function* () { + const agents = yield* AgentV2.Service + const build = AgentV2.ID.make("build") + const defaults = yield* agents.transform() + + yield* defaults((editor) => + editor.update(build, (agent) => { + agent.mode = "primary" + agent.permissions.push({ permission: "bash", pattern: "*", action: "allow" }) + }), + ) + + const config = Config.Service.of({ + directories: () => Effect.succeed([]), + get: () => + Effect.succeed([ + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + permissions: [{ permission: "bash", pattern: "*", action: "ask" }], + agents: { + build: { + permissions: [{ permission: "bash", pattern: "git *", action: "allow" }], + }, + reviewer: { + model: "openrouter/openai/gpt-5", + description: "Review changes", + mode: "subagent", + permissions: [{ permission: "edit", pattern: "*", action: "deny" }], + }, + removed: { description: "Removed later" }, + }, + }), + }), + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + agents: { + reviewer: { variant: "high", hidden: true }, + removed: { disabled: true }, + }, + }), + }), + ]), + }) + + yield* ConfigAgentPlugin.Plugin.effect.pipe( + Effect.provideService(Config.Service, config), + Effect.provideService(AgentV2.Service, agents), + ) + + const buildAgent = yield* agents.get(build) + if (!buildAgent) throw new Error("expected configured build agent") + expect(buildAgent.permissions).toEqual([ + { permission: "bash", pattern: "*", action: "allow" }, + { permission: "bash", pattern: "*", action: "ask" }, + { permission: "bash", pattern: "git *", action: "allow" }, + ]) + expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).action).toBe("allow") + expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).action).toBe("ask") + + const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + if (!reviewer) throw new Error("expected configured reviewer agent") + expect(reviewer).toMatchObject({ + description: "Review changes", + mode: "subagent", + hidden: true, + model: { providerID: "openrouter", id: "openai/gpt-5", variant: "high" }, + }) + expect(reviewer.permissions).toEqual([ + { permission: "bash", pattern: "*", action: "ask" }, + { permission: "edit", pattern: "*", action: "deny" }, + ]) + expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined() + }), + ) + + it.effect("maps configured agent fields and preserves an unspecified model variant", () => + Effect.gen(function* () { + const agents = yield* AgentV2.Service + const config = Config.Service.of({ + directories: () => Effect.succeed([]), + get: () => + Effect.succeed([ + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + agents: { + reviewer: { + model: "anthropic/claude-sonnet", + system: "Review carefully.", + description: "Reviews changes", + mode: "subagent", + hidden: true, + color: "warning", + steps: 12, + options: { + headers: { first: "one", shared: "first" }, + body: { enabled: true }, + aisdk: { provider: { profile: "review" }, request: { effort: "medium" } }, + }, + }, + }, + }), + }), + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + agents: { + reviewer: { + options: { + headers: { shared: "last", second: "two" }, + body: { retries: 2 }, + aisdk: { request: { effort: "high" } }, + }, + }, + }, + }), + }), + ]), + }) + + yield* ConfigAgentPlugin.Plugin.effect.pipe( + Effect.provideService(Config.Service, config), + Effect.provideService(AgentV2.Service, agents), + ) + + const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + if (!reviewer) throw new Error("expected configured reviewer agent") + expect(reviewer).toMatchObject({ + system: "Review carefully.", + description: "Reviews changes", + mode: "subagent", + hidden: true, + color: "warning", + steps: 12, + model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined }, + }) + expect(reviewer.options).toEqual({ + headers: { first: "one", shared: "last", second: "two" }, + body: { enabled: true, retries: 2 }, + aisdk: { provider: { profile: "review" }, request: { effort: "high" } }, + }) + }), + ) + + it.effect("removes a built-in agent disabled by configuration", () => + Effect.gen(function* () { + const agents = yield* AgentV2.Service + const build = AgentV2.ID.make("build") + const defaults = yield* agents.transform() + yield* defaults((editor) => editor.update(build, () => {})) + + const config = Config.Service.of({ + directories: () => Effect.succeed([]), + get: () => + Effect.succeed([ + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ agents: { build: { disabled: true } } }), + }), + ]), + }) + + yield* ConfigAgentPlugin.Plugin.effect.pipe( + Effect.provideService(Config.Service, config), + Effect.provideService(AgentV2.Service, agents), + ) + + expect(yield* agents.get(build)).toBeUndefined() + }), + ) +}) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts new file mode 100644 index 00000000000..21f9e3e3d11 --- /dev/null +++ b/packages/core/test/config/config.test.ts @@ -0,0 +1,460 @@ +import path from "path" +import fs from "fs/promises" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { Config } from "@opencode-ai/core/config" +import { ConfigProvider } from "@opencode-ai/core/config/provider" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { Location } from "@opencode-ai/core/location" +import { Policy } from "@opencode-ai/core/policy" +import { Project } from "@opencode-ai/core/project" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" +import { tmpdir } from "../fixture/tmpdir" +import { testEffect } from "../lib/effect" + +const it = testEffect(Layer.empty) + +function testLayer( + directory: string, + globalDirectory = path.join(directory, "global"), + projectDirectory = directory, + vcs?: Project.Vcs, +) { + return Config.layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Global.layerWith({ config: globalDirectory })), + Layer.provideMerge(Policy.defaultLayer), + Layer.provide( + Layer.succeed( + Location.Service, + Location.Service.of( + location( + { directory: AbsolutePath.make(directory) }, + { projectDirectory: AbsolutePath.make(projectDirectory), vcs }, + ), + ), + ), + ), + ) +} + +const provider = { + endpoint: { type: "unknown" }, + options: { + headers: {}, + body: {}, + aisdk: { + provider: {}, + request: {}, + }, + }, + models: {}, +} + +describe("Config", () => { + it.live("returns an empty configuration when directory files do not exist", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const config = yield* Config.Service + const documents = yield* config.get() + + expect(documents).toEqual([]) + }).pipe(Effect.provide(testLayer(tmp.path))), + ), + ), + ) + + it.live("loads JSON and JSONC files from lowest to highest priority", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + Promise.all([ + fs.writeFile( + path.join(tmp.path, "config.json"), + JSON.stringify({ $schema: "base", providers: { base: provider } }), + ), + fs.writeFile( + path.join(tmp.path, "opencode.json"), + JSON.stringify({ $schema: "middle", providers: { middle: provider } }), + ), + fs.writeFile( + path.join(tmp.path, "opencode.jsonc"), + `{ + // Later global files override scalar fields while retaining providers. + "$schema": "last", + "providers": { "last": ${JSON.stringify(provider)} }, + }`, + ), + ]), + ) + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = yield* config.get() + + expect(documents).toHaveLength(3) + expect(documents.map((document) => document.source.type)).toEqual(["file", "file", "file"]) + expect(documents.map((document) => document.info.$schema)).toEqual(["base", "middle", "last"]) + expect(documents[0]).toBeInstanceOf(Config.Loaded) + expect(documents[0]?.source.type === "file" ? documents[0].source.path : undefined).toBe( + path.join(tmp.path, "config.json"), + ) + expect(documents[2]?.info.providers?.last).toBeInstanceOf(ConfigProvider.Info) + + yield* Effect.promise(() => + fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ $schema: "changed" })), + ) + expect((yield* config.get()).map((document) => document.info.$schema)).toEqual(["base", "middle", "last"]) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + + it.live("accepts $schema metadata without writing it into config files", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const file = path.join(tmp.path, "opencode.json") + const contents = JSON.stringify({ + shell: "/bin/zsh", + experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] }, + providers: { local: provider }, + }) + yield* Effect.promise(() => fs.writeFile(file, contents)) + + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = yield* config.get() + + expect(documents[0]?.info.$schema).toBeUndefined() + expect(documents[0]?.info.shell).toBe("/bin/zsh") + expect(documents[0]?.info.experimental?.policies?.[0]).toEqual({ + effect: "deny", + action: "provider.use", + resource: "openai", + }) + expect(yield* Effect.promise(() => fs.readFile(file, "utf8"))).toBe(contents) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + + it.live("loads supported scalar and resource configuration", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + fs.writeFile( + path.join(tmp.path, "opencode.json"), + JSON.stringify({ + shell: "/bin/bash", + model: "anthropic/claude", + autoupdate: "notify", + share: "disabled", + enterprise: { url: "https://share.example.com" }, + username: "test-user", + permissions: [ + { permission: "bash", pattern: "*", action: "ask" }, + { permission: "bash", pattern: "git status", action: "allow" }, + ], + agents: { + reviewer: { + model: "openrouter/openai/gpt-5", + variant: "high", + options: { + headers: { "x-agent": "reviewer" }, + aisdk: { request: { reasoningEffort: "high" } }, + }, + description: "Review changes for correctness", + system: "Find regressions.", + mode: "subagent", + hidden: false, + color: "warning", + steps: 12, + disabled: false, + permissions: [{ permission: "edit", pattern: "*", action: "deny" }], + }, + }, + snapshots: false, + watcher: { ignore: ["node_modules/**", "dist/**", ".git"] }, + formatter: { + prettier: { disabled: true }, + custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] }, + }, + lsp: { typescript: { disabled: true }, custom: { command: ["custom-lsp"], extensions: [".foo"] } }, + attachments: { + image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 }, + }, + tool_output: { max_lines: 1000, max_bytes: 32768 }, + mcp: { + timeout: 5000, + servers: { + local: { + type: "local", + command: ["node", "./mcp/server.js"], + environment: { API_KEY: "secret" }, + disabled: false, + timeout: 10000, + }, + remote: { + type: "remote", + url: "https://mcp.example.com/mcp", + headers: { Authorization: "Bearer token" }, + oauth: { client_id: "client", scope: "read write", callback_port: 19876 }, + disabled: true, + }, + }, + }, + compaction: { + auto: true, + prune: false, + keep: { turns: 3, tokens: 2000 }, + buffer: 10000, + }, + skills: ["./skills", "~/shared-skills", "https://example.com/.well-known/skills/"], + instructions: ["CONTRIBUTING.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"], + references: { + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }, + plugins: [ + "opencode-helicone-session", + { package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } }, + ], + }), + ), + ) + + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = yield* config.get() + + expect(documents).toHaveLength(1) + expect(documents[0]?.info.shell).toBe("/bin/bash") + expect(documents[0]?.info.model).toBe("anthropic/claude") + expect(documents[0]?.info.autoupdate).toBe("notify") + expect(documents[0]?.info.share).toBe("disabled") + expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" }) + expect(documents[0]?.info.username).toBe("test-user") + expect(documents[0]?.info.permissions).toEqual([ + { permission: "bash", pattern: "*", action: "ask" }, + { permission: "bash", pattern: "git status", action: "allow" }, + ]) + expect(documents[0]?.info.agents?.reviewer).toEqual({ + model: "openrouter/openai/gpt-5", + variant: "high", + options: { + headers: { "x-agent": "reviewer" }, + aisdk: { request: { reasoningEffort: "high" } }, + }, + description: "Review changes for correctness", + system: "Find regressions.", + mode: "subagent", + hidden: false, + color: "warning", + steps: 12, + disabled: false, + permissions: [{ permission: "edit", pattern: "*", action: "deny" }], + }) + expect(documents[0]?.info.snapshots).toBe(false) + expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] }) + expect(documents[0]?.info.formatter).toEqual({ + prettier: { disabled: true }, + custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] }, + }) + expect(documents[0]?.info.lsp).toEqual({ + typescript: { disabled: true }, + custom: { command: ["custom-lsp"], extensions: [".foo"] }, + }) + expect(documents[0]?.info.attachments).toEqual({ + image: { auto_resize: false, max_width: 1200, max_height: 900, max_base64_bytes: 1048576 }, + }) + expect(documents[0]?.info.tool_output).toEqual({ max_lines: 1000, max_bytes: 32768 }) + expect(documents[0]?.info.mcp).toEqual({ + timeout: 5000, + servers: { + local: { + type: "local", + command: ["node", "./mcp/server.js"], + environment: { API_KEY: "secret" }, + disabled: false, + timeout: 10000, + }, + remote: { + type: "remote", + url: "https://mcp.example.com/mcp", + headers: { Authorization: "Bearer token" }, + oauth: { client_id: "client", scope: "read write", callback_port: 19876 }, + disabled: true, + }, + }, + }) + expect(documents[0]?.info.compaction).toEqual({ + auto: true, + prune: false, + keep: { turns: 3, tokens: 2000 }, + buffer: 10000, + }) + expect(documents[0]?.info.skills).toEqual([ + "./skills", + "~/shared-skills", + "https://example.com/.well-known/skills/", + ]) + expect(documents[0]?.info.instructions).toEqual([ + "CONTRIBUTING.md", + ".cursor/rules/*.md", + "https://example.com/shared-rules.md", + ]) + expect(documents[0]?.info.references).toEqual({ + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }) + expect(documents[0]?.info.plugins).toEqual([ + "opencode-helicone-session", + { package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } }, + ]) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + + it.live("ignores invalid files while loading valid config values", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + Promise.all([ + fs.writeFile(path.join(tmp.path, "config.json"), JSON.stringify({ $schema: "base" })), + fs.writeFile(path.join(tmp.path, "opencode.json"), "{ invalid"), + fs.writeFile(path.join(tmp.path, "opencode.jsonc"), JSON.stringify({ providers: { invalid: true } })), + ]), + ) + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = yield* config.get() + + expect(documents.map((document) => document.info.$schema)).toEqual(["base"]) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + + it.live("loads policy statements in reverse config order", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => { + const global = path.join(tmp.path, "global") + return Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(global, { recursive: true }) + await fs.writeFile( + path.join(global, "opencode.json"), + JSON.stringify({ + experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] }, + }), + ) + await fs.writeFile( + path.join(tmp.path, "opencode.json"), + JSON.stringify({ + experimental: { policies: [{ effect: "allow", action: "provider.use", resource: "openai" }] }, + }), + ) + }) + + return yield* Effect.gen(function* () { + const policy = yield* Policy.Service + + expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny") + }).pipe(Effect.provide(testLayer(tmp.path, global))) + }) + }), + ), + ) + + it.live("loads global, ancestor, and .opencode configuration up to the project boundary", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => { + const global = path.join(tmp.path, "global") + const root = path.join(tmp.path, "repo") + const parent = path.join(root, "packages") + const directory = path.join(parent, "app") + return Effect.gen(function* () { + yield* Effect.promise(async () => { + await fs.mkdir(global, { recursive: true }) + await fs.mkdir(directory, { recursive: true }) + await fs.mkdir(path.join(root, ".opencode"), { recursive: true }) + await fs.mkdir(path.join(directory, ".opencode"), { recursive: true }) + await Promise.all([ + fs.writeFile(path.join(tmp.path, "opencode.json"), JSON.stringify({ $schema: "outside" })), + fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ $schema: "global" })), + fs.writeFile(path.join(root, "opencode.json"), JSON.stringify({ $schema: "root" })), + fs.writeFile(path.join(parent, "opencode.jsonc"), JSON.stringify({ $schema: "parent" })), + fs.writeFile(path.join(directory, "config.json"), JSON.stringify({ $schema: "directory" })), + fs.writeFile(path.join(root, ".opencode", "opencode.json"), JSON.stringify({ $schema: "root-dot" })), + fs.writeFile( + path.join(directory, ".opencode", "opencode.jsonc"), + JSON.stringify({ $schema: "directory-dot" }), + ), + ]) + }) + + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const directories = yield* config.directories() + const documents = yield* config.get() + + expect(directories).toEqual([ + AbsolutePath.make(global), + AbsolutePath.make(path.join(root, ".opencode")), + AbsolutePath.make(path.join(directory, ".opencode")), + ]) + expect(documents.map((document) => document.info.$schema)).toEqual([ + "global", + "root", + "parent", + "directory", + "root-dot", + "directory-dot", + ]) + }).pipe( + Effect.provide( + testLayer(directory, global, root, { + type: "git", + store: AbsolutePath.make(path.join(root, ".git")), + }), + ), + ) + }) + }), + ), + ) +}) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts new file mode 100644 index 00000000000..01b8c9c9aa3 --- /dev/null +++ b/packages/core/test/config/provider.test.ts @@ -0,0 +1,131 @@ +import { describe, expect } from "bun:test" +import { Effect, Schema } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" +import { Config } from "@opencode-ai/core/config" +import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" +import { ModelV2 } from "@opencode-ai/core/model" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { it } from "../plugin/provider-helper" + +function options(headers: Record, variant?: string) { + return { + headers, + variant, + } +} + +const decode = Schema.decodeUnknownSync(Config.Info) + +describe("ConfigProviderPlugin.Plugin", () => { + it.effect("loads configured providers and applies later model overrides", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const plugin = yield* PluginV2.Service + const providerID = ProviderV2.ID.make("custom") + const modelID = ModelV2.ID.make("chat") + const config = Config.Service.of({ + directories: () => Effect.succeed([]), + get: () => + Effect.succeed([ + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + providers: { + custom: { + name: "Configured", + env: ["CUSTOM_API_KEY"], + endpoint: { type: "unknown" }, + options: options({ first: "first", shared: "first" }), + models: { + chat: { + name: "First", + capabilities: { tools: true, input: ["text"], output: ["text"] }, + disabled: true, + limit: { context: 100, output: 50 }, + cost: { input: 1, output: 2 }, + options: options({ first: "first", shared: "first" }, "retained"), + variants: [ + { + id: "fast", + headers: { first: "first", shared: "first" }, + }, + ], + }, + }, + }, + }, + }), + }), + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + providers: { + custom: { + endpoint: { type: "aisdk", package: "custom-sdk", url: "https://example.test" }, + options: options({ last: "last", shared: "last" }), + models: { + chat: { + api_id: "api-chat", + name: "Last", + limit: { output: 75 }, + options: options({ last: "last", shared: "last" }), + variants: [ + { + id: "fast", + headers: { last: "last", shared: "last" }, + }, + { + id: "slow", + headers: { slow: "slow" }, + }, + ], + }, + }, + }, + }, + }), + }), + new Config.Loaded({ + source: { type: "memory" }, + info: decode({ + providers: { + custom: { name: "Renamed" }, + }, + }), + }), + ]), + }) + + yield* plugin.add({ + ...ConfigProviderPlugin.Plugin, + effect: ConfigProviderPlugin.Plugin.effect.pipe( + Effect.provideService(Config.Service, config), + Effect.provideService(Catalog.Service, catalog), + ), + }) + + const provider = yield* catalog.provider.get(providerID) + const model = yield* catalog.model.get(providerID, modelID) + expect(provider.name).toBe("Renamed") + expect(provider.env).toEqual(["CUSTOM_API_KEY"]) + expect(provider.enabled).toEqual({ via: "custom", data: {} }) + expect(provider.endpoint).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" }) + expect(provider.options.headers).toEqual({ first: "first", shared: "last", last: "last" }) + expect(model.apiID).toBe(ModelV2.ID.make("api-chat")) + expect(model.name).toBe("Last") + expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) + expect(model.enabled).toBe(false) + expect(model.limit).toEqual({ context: 100, output: 75 }) + expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }]) + expect(model.options.headers).toEqual({ first: "first", shared: "last", last: "last" }) + expect(model.options.variant).toBe("retained") + expect(model.variants.map((variant) => variant.id)).toEqual([ + ModelV2.VariantID.make("fast"), + ModelV2.VariantID.make("slow"), + ]) + expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" }) + expect(model.variants[1]?.headers).toEqual({ slow: "slow" }) + }), + ) +}) diff --git a/packages/core/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts index e394d43bf84..a3f74f671d9 100644 --- a/packages/core/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -111,13 +111,7 @@ describe("cross-spawn spawner", () => { ChildProcess.make(process.execPath, ["-e", "process.stdout.write(process.cwd())"], { cwd: tmp.path }), ), ) - const cwd = yield* Effect.promise(() => fs.realpath(tmp.path)) - const actual = yield* Effect.promise(() => fs.realpath(out)) - const normalize = (value: string) => { - const normalized = path.normalize(value) - return process.platform === "win32" ? normalized.toLowerCase() : normalized - } - expect(normalize(actual)).toBe(normalize(cwd)) + expect(yield* Effect.promise(() => fs.realpath(out))).toBe(yield* Effect.promise(() => fs.realpath(tmp.path))) }), ) @@ -125,7 +119,9 @@ describe("cross-spawn spawner", () => { "fails for invalid cwd", Effect.gen(function* () { const exit = yield* Effect.exit( - ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" }).asEffect(), + ChildProcessSpawner.ChildProcessSpawner.use((svc) => + svc.spawn(ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" })), + ), ) expect(Exit.isFailure(exit)).toBe(true) }), diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index b67b2897a1b..f5cce54de41 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -2,11 +2,13 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer, Schema, Stream } from "effect" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" import { testEffect } from "./lib/effect" const locationLayer = Layer.succeed( Location.Service, - Location.Service.of({ directory: "project", workspaceID: "workspace" }), + Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: "workspace" })), ) const it = testEffect(EventV2.layer.pipe(Layer.provideMerge(locationLayer))) const itWithoutLocation = testEffect(EventV2.layer) @@ -46,7 +48,7 @@ describe("EventV2", () => { expect(event.type).toBe("test.message") expect(event).not.toHaveProperty("version") expect(event.data).toEqual({ text: "hello" }) - expect(event.location).toEqual({ directory: "project", workspaceID: "workspace" }) + expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: "workspace" }) }), ) diff --git a/packages/core/test/fixture/location.ts b/packages/core/test/fixture/location.ts new file mode 100644 index 00000000000..00b3ffbd13f --- /dev/null +++ b/packages/core/test/fixture/location.ts @@ -0,0 +1,12 @@ +import { Location } from "@opencode-ai/core/location" +import { Project } from "@opencode-ai/core/project" +import { AbsolutePath } from "@opencode-ai/core/schema" + +export function location(ref: Location.Ref, input: { projectDirectory?: AbsolutePath; vcs?: Project.Vcs } = {}) { + return { + directory: ref.directory, + workspaceID: ref.workspaceID, + project: { id: Project.ID.global, directory: input.projectDirectory ?? ref.directory }, + vcs: input.vcs, + } satisfies Location.Interface +} diff --git a/packages/core/test/kilocode/account-auth-v2-migration.test.ts b/packages/core/test/kilocode/account-auth-v2-migration.test.ts new file mode 100644 index 00000000000..0f12b338e1e --- /dev/null +++ b/packages/core/test/kilocode/account-auth-v2-migration.test.ts @@ -0,0 +1,97 @@ +import path from "path" +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { AccountV2 } from "@opencode-ai/core/account" +import { EventV2 } from "@opencode-ai/core/event" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { tmpdir } from "../fixture/tmpdir" +import { it } from "../lib/effect" + +function layer(dir: string) { + return AccountV2.layer.pipe( + Layer.provide(AppFileSystem.defaultLayer), + Layer.provideMerge(EventV2.defaultLayer), + Layer.provide(Global.layerWith({ data: dir })), + ) +} + +const auth = Effect.acquireRelease( + Effect.sync(() => { + const value = process.env.KILO_AUTH_CONTENT + delete process.env.KILO_AUTH_CONTENT + return value + }), + (value) => + Effect.sync(() => { + if (value === undefined) delete process.env.KILO_AUTH_CONTENT + else process.env.KILO_AUTH_CONTENT = value + }), +) + +describe("AccountV2 auth-v2 migration", () => { + it.live("preserves multiple accounts, active selection, and Kilo organization", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + auth.pipe( + Effect.flatMap(() => + Effect.gen(function* () { + const store = { + version: 2, + accounts: { + acc_first: { + id: "acc_first", + serviceID: "kilo", + description: "first", + credential: { + type: "oauth", + refresh: "refresh-first", + access: "access-first", + expires: 1, + accountId: "org-first", + }, + }, + acc_second: { + id: "acc_second", + serviceID: "kilo", + description: "second", + credential: { + type: "oauth", + refresh: "refresh-second", + access: "access-second", + expires: 2, + accountId: "org-second", + }, + }, + }, + active: { kilo: "acc_second" }, + } + yield* Effect.promise(() => Bun.write(path.join(tmp.path, "auth-v2.json"), JSON.stringify(store))) + + const result = yield* Effect.gen(function* () { + const accounts = yield* AccountV2.Service + return { + all: yield* accounts.all(), + active: yield* accounts.active(AccountV2.ServiceID.make("kilo")), + } + }).pipe(Effect.provide(layer(tmp.path))) + + expect(result.all.map((item) => String(item.id))).toEqual(["acc_first", "acc_second"]) + expect(String(result.active?.id)).toBe("acc_second") + expect(result.active?.credential.type).toBe("oauth") + if (result.active?.credential.type === "oauth") { + expect(result.active.credential.access).toBe("access-second") + expect(result.active.credential.accountId).toBe("org-second") + } + const saved = yield* Effect.promise(() => Bun.file(path.join(tmp.path, "account.json")).json()) + expect(saved).toEqual(store) + }), + ), + ), + ), + ), + ) +}) diff --git a/packages/core/test/kilocode/linux-sandbox.test.ts b/packages/core/test/kilocode/linux-sandbox.test.ts new file mode 100644 index 00000000000..83d5b43032a --- /dev/null +++ b/packages/core/test/kilocode/linux-sandbox.test.ts @@ -0,0 +1,739 @@ +import { expect, test } from "bun:test" +import { spawnSync } from "node:child_process" +import { createSocket } from "node:dgram" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { Effect } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import { backendSupport, run, type Profile } from "@kilocode/sandbox" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" + +const linux = process.platform === "linux" ? test : test.skip +const linuxIPv6 = process.platform === "linux" && supportsIPv6() ? test : test.skip + +function profile( + allow: ReadonlyArray, + denyNames: ReadonlyArray = [], + mode: Profile["network"]["mode"] = "allow", +): Profile { + return { + filesystem: { + allowWrite: allow.map((path) => ({ path, kind: "subtree" })), + denyWrite: [], + denyNames, + }, + network: { mode, allowedHosts: [] }, + environment: { deny: [], set: {} }, + } +} + +function denied(base: Profile, rules: Profile["filesystem"]["denyWrite"]): Profile { + return { ...base, filesystem: { ...base.filesystem, denyWrite: rules } } +} + +function spawn(script: string, cwd: string, policy: Profile) { + return Effect.scoped( + run( + policy, + ChildProcessSpawner.ChildProcessSpawner.use((spawner) => + spawner + .spawn(ChildProcess.make(process.execPath, ["-e", script], { cwd })) + .pipe(Effect.flatMap((handle) => handle.exitCode)), + ), + ).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)), + ) +} + +async function fixture() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-linux-sandbox-")) + const project = path.join(root, "project") + const outside = path.join(root, "outside") + await fs.mkdir(project) + await fs.mkdir(outside) + return { root, project, outside } +} + +function requireNetwork() { + const support = backendSupport({ mode: "deny", allowedHosts: [] }) + expect(support.available, support.reason).toBe(true) +} + +function tcp(hostname = "127.0.0.1") { + let accepted = 0 + const listener = Bun.listen({ + hostname, + port: 0, + socket: { + open(socket) { + accepted++ + socket.write("sandbox-tcp-ok") + socket.end() + }, + data() {}, + }, + }) + return { listener, accepted: () => accepted } +} + +function supportsIPv6() { + if (process.platform !== "linux") return false + try { + const probe = tcp("::1") + probe.listener.stop(true) + return true + } catch { + return false + } +} + +async function udp() { + let received = 0 + const socket = createSocket("udp4") + socket.on("message", (_message, peer) => { + received++ + socket.send("sandbox-udp-ok", peer.port, peer.address) + }) + await new Promise((resolve, reject) => { + socket.once("error", reject) + socket.bind(0, "127.0.0.1", () => { + socket.off("error", reject) + resolve() + }) + }) + const address = socket.address() + if (typeof address === "string") throw new Error("UDP server did not expose an IP address") + return { socket, port: address.port, received: () => received } +} + +function tcpClient(port: number, expected: boolean, hostname = "127.0.0.1") { + return [ + 'const net = require("node:net")', + `const socket = net.connect({ host: ${JSON.stringify(hostname)}, port: ${port} })`, + `const expected = ${expected}`, + 'socket.on("data", (data) => process.exit(expected && data.toString() === "sandbox-tcp-ok" ? 0 : 2))', + 'socket.on("error", () => process.exit(expected ? 3 : 0))', + "setTimeout(() => process.exit(expected ? 4 : 0), 1000)", + ].join("\n") +} + +function udpClient(port: number, expected: boolean) { + return [ + 'const dgram = require("node:dgram")', + 'const socket = dgram.createSocket("udp4")', + `const expected = ${expected}`, + 'socket.on("message", (data) => process.exit(expected && data.toString() === "sandbox-udp-ok" ? 0 : 2))', + 'socket.on("error", () => process.exit(expected ? 3 : 0))', + `socket.send("probe", ${port}, "127.0.0.1", (error) => { if (error) process.exit(expected ? 4 : 0) })`, + "setTimeout(() => process.exit(expected ? 5 : 0), 500)", + ].join("\n") +} + +linux("confines writes from spawned processes to the profile allowlist", async () => { + const support = backendSupport() + expect(support.available, support.reason).toBe(true) + const root = await fixture() + const allowed = path.join(root.project, "allowed.txt") + const sentinel = path.join(root.outside, "sentinel.txt") + await fs.writeFile(sentinel, "original") + + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(sentinel)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project]))))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(sentinel, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("allows host loopback TCP in network allow mode and blocks it in deny mode", async () => { + requireNetwork() + const root = await fixture() + const allowed = tcp() + const blocked = tcp() + + try { + const allow = profile([root.project], [], "allow") + const deny = profile([root.project], [], "deny") + expect(Number(await Effect.runPromise(spawn(tcpClient(allowed.listener.port, true), root.project, allow)))).toBe(0) + expect(Number(await Effect.runPromise(spawn(tcpClient(blocked.listener.port, false), root.project, deny)))).toBe(0) + expect(allowed.accepted()).toBe(1) + expect(blocked.accepted()).toBe(0) + } finally { + allowed.listener.stop(true) + blocked.listener.stop(true) + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("blocks UDP datagrams in network deny mode", async () => { + requireNetwork() + const root = await fixture() + const allowed = await udp() + const blocked = await udp() + + try { + const allow = profile([root.project], [], "allow") + const deny = profile([root.project], [], "deny") + expect(Number(await Effect.runPromise(spawn(udpClient(allowed.port, true), root.project, allow)))).toBe(0) + expect(Number(await Effect.runPromise(spawn(udpClient(blocked.port, false), root.project, deny)))).toBe(0) + expect(allowed.received()).toBe(1) + expect(blocked.received()).toBe(0) + } finally { + allowed.socket.close() + blocked.socket.close() + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("blocks localhost connections in network deny mode", async () => { + requireNetwork() + const root = await fixture() + const listener = tcp() + const deny = profile([root.project], [], "deny") + + try { + expect( + Number(await Effect.runPromise(spawn(tcpClient(listener.listener.port, false, "localhost"), root.project, deny))), + ).toBe(0) + expect(listener.accepted()).toBe(0) + } finally { + listener.listener.stop(true) + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linuxIPv6("blocks IPv6 loopback connections in network deny mode", async () => { + requireNetwork() + const root = await fixture() + const listener = tcp("::1") + const deny = profile([root.project], [], "deny") + + try { + expect( + Number(await Effect.runPromise(spawn(tcpClient(listener.listener.port, false, "::1"), root.project, deny))), + ).toBe(0) + expect(listener.accepted()).toBe(0) + } finally { + listener.listener.stop(true) + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps loopback available between processes inside the denied network namespace", async () => { + requireNetwork() + const root = await fixture() + const child = [ + 'const net = require("node:net")', + "const port = Number(process.argv[1])", + 'const socket = net.connect({ host: "127.0.0.1", port })', + 'socket.on("connect", () => socket.write("sandbox-internal-ok"))', + 'socket.on("data", (data) => process.exit(data.toString() === "sandbox-internal-ok" ? 0 : 2))', + 'socket.on("error", () => process.exit(3))', + ].join("\n") + const script = [ + 'const child = require("node:child_process")', + 'const net = require("node:net")', + 'const server = net.createServer((socket) => socket.on("data", (data) => socket.end(data)))', + 'server.listen(0, "127.0.0.1", () => {', + ` const proc = child.spawn(process.execPath, ["-e", ${JSON.stringify(child)}, String(server.address().port)])`, + ' proc.on("exit", (code) => process.exit(code ?? 4))', + "})", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [], "deny"))))).toBe(0) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps descendants in the denied network namespace", async () => { + requireNetwork() + const root = await fixture() + const blocked = tcp() + const child = tcpClient(blocked.listener.port, false) + const script = [ + 'const child = require("node:child_process")', + `const result = child.spawnSync(process.execPath, ["-e", ${JSON.stringify(child)}])`, + "process.exit(result.status ?? 3)", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [], "deny"))))).toBe(0) + expect(blocked.accepted()).toBe(0) + } finally { + blocked.listener.stop(true) + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("preserves filesystem confinement in network deny mode", async () => { + requireNetwork() + const root = await fixture() + const allowed = path.join(root.project, "network-deny.txt") + const sentinel = path.join(root.outside, "network-deny.txt") + await fs.writeFile(sentinel, "original") + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(sentinel)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [], "deny"))))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(sentinel, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps reads available when no paths are writable", async () => { + const root = await fixture() + const sentinel = path.join(root.project, "sentinel.txt") + await fs.writeFile(sentinel, "original") + const script = [ + 'const fs = require("node:fs")', + `if (fs.readFileSync(${JSON.stringify(sentinel)}, "utf8") !== "original") process.exit(2)`, + "try {", + ` fs.writeFileSync(${JSON.stringify(sentinel)}, "escaped")`, + " process.exit(3)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([]))))).toBe(0) + expect(await fs.readFile(sentinel, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps existing git metadata read-only under a writable project", async () => { + const root = await fixture() + const git = path.join(root.project, ".git") + const config = path.join(git, "config") + const allowed = path.join(root.project, "allowed.txt") + await fs.mkdir(git) + await fs.writeFile(config, "original") + + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(config)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [".git"]))))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(config, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps existing nested git metadata read-only", async () => { + const root = await fixture() + const git = path.join(root.project, "packages", "nested", ".git") + const config = path.join(git, "config") + const allowed = path.join(root.project, "allowed.txt") + await fs.mkdir(git, { recursive: true }) + await fs.writeFile(config, "original") + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(config)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [".git"]))))).toBe(0) + expect(await fs.readFile(config, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("keeps worktree git marker files read-only", async () => { + const root = await fixture() + const marker = path.join(root.project, ".git") + const renamed = path.join(root.project, ".git-moved") + await fs.writeFile(marker, "gitdir: /outside") + const script = [ + 'const fs = require("node:fs")', + "let blocked = 0", + `try { fs.writeFileSync(${JSON.stringify(marker)}, "escaped") } catch { blocked++ }`, + `try { fs.renameSync(${JSON.stringify(marker)}, ${JSON.stringify(renamed)}) } catch { blocked++ }`, + "process.exit(blocked === 2 ? 0 : 2)", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project], [".git"]))))).toBe(0) + expect(await fs.readFile(marker, "utf8")).toBe("gitdir: /outside") + expect( + await fs.stat(renamed).then( + () => true, + () => false, + ), + ).toBe(false) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("applies explicit file and subtree denies after a writable parent", async () => { + const root = await fixture() + const file = path.join(root.project, "protected.txt") + const dir = path.join(root.project, "protected") + const nested = path.join(dir, "value.txt") + const allowed = path.join(root.project, "allowed.txt") + await fs.writeFile(file, "original") + await fs.mkdir(dir) + await fs.writeFile(nested, "original") + const policy = denied(profile([root.project]), [ + { path: file, kind: "literal" }, + { path: dir, kind: "subtree" }, + ]) + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "let blocked = 0", + `try { fs.writeFileSync(${JSON.stringify(file)}, "escaped") } catch { blocked++ }`, + `try { fs.writeFileSync(${JSON.stringify(nested)}, "escaped") } catch { blocked++ }`, + "process.exit(blocked === 2 ? 0 : 2)", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, policy)))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(file, "utf8")).toBe("original") + expect(await fs.readFile(nested, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("supports writable literal files without opening writable siblings", async () => { + const root = await fixture() + const allowed = path.join(root.project, "allowed.txt") + const sibling = path.join(root.project, "sibling.txt") + await fs.writeFile(allowed, "original") + await fs.writeFile(sibling, "original") + const base = profile([]) + const policy: Profile = { + ...base, + filesystem: { ...base.filesystem, allowWrite: [{ path: allowed, kind: "literal" }] }, + } + const script = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(sibling)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, policy)))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(sibling, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("blocks writes through a project symlink to an outside path", async () => { + const root = await fixture() + const sentinel = path.join(root.outside, "sentinel.txt") + const link = path.join(root.project, "outside") + await fs.writeFile(sentinel, "original") + await fs.symlink(root.outside, link) + + const script = [ + 'const fs = require("node:fs")', + "try {", + ` fs.writeFileSync(${JSON.stringify(path.join(link, "sentinel.txt"))}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project]))))).toBe(0) + expect(await fs.readFile(sentinel, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("allows every profile root including configured temp and cache paths", async () => { + const root = await fixture() + const temp = path.join(root.root, "temp") + const cache = path.join(root.root, "cache") + await fs.mkdir(temp) + await fs.mkdir(cache) + const base = profile([root.project, temp, cache]) + const policy: Profile = { + ...base, + filesystem: { ...base.filesystem, temporaryDirectory: temp }, + environment: { ...base.environment, set: { TMPDIR: temp } }, + } + + const files = [path.join(root.project, "project.txt"), path.join(temp, "temp.txt"), path.join(cache, "cache.txt")] + const script = [ + 'const fs = require("node:fs")', + ...files.map((file) => `fs.writeFileSync(${JSON.stringify(file)}, "allowed")`), + `if (process.env.TMPDIR !== ${JSON.stringify(temp)}) process.exit(2)`, + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, policy)))).toBe(0) + expect(await Promise.all(files.map((file) => fs.readFile(file, "utf8")))).toEqual(["allowed", "allowed", "allowed"]) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("applies the profile environment without inheriting denied values", async () => { + const root = await fixture() + const base = profile([root.project]) + const policy: Profile = { + ...base, + environment: { deny: ["KILO_SANDBOX_DENIED"], set: { KILO_SANDBOX_SET: "expected" } }, + } + const script = [ + 'if (process.env.KILO_SANDBOX_SET !== "expected") process.exit(2)', + "if (process.env.KILO_SANDBOX_DENIED !== undefined) process.exit(3)", + ].join("\n") + + try { + const effect = Effect.scoped( + run( + policy, + ChildProcessSpawner.ChildProcessSpawner.use((spawner) => + spawner + .spawn( + ChildProcess.make(process.execPath, ["-e", script], { + cwd: root.project, + env: { KILO_SANDBOX_DENIED: "ambient" }, + extendEnv: true, + }), + ) + .pipe(Effect.flatMap((handle) => handle.exitCode)), + ), + ).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)), + ) + expect(Number(await Effect.runPromise(effect))).toBe(0) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("confines writes from descendant processes", async () => { + const root = await fixture() + const allowed = path.join(root.project, "child.txt") + const sentinel = path.join(root.outside, "sentinel.txt") + await fs.writeFile(sentinel, "original") + const child = [ + 'const fs = require("node:fs")', + `fs.writeFileSync(${JSON.stringify(allowed)}, "allowed")`, + "try {", + ` fs.writeFileSync(${JSON.stringify(sentinel)}, "escaped")`, + " process.exit(2)", + "} catch {", + " process.exit(0)", + "}", + ].join("\n") + const script = [ + 'const child = require("node:child_process")', + `const result = child.spawnSync(process.execPath, ["-e", ${JSON.stringify(child)}])`, + "process.exit(result.status ?? 3)", + ].join("\n") + + try { + expect(Number(await Effect.runPromise(spawn(script, root.project, profile([root.project]))))).toBe(0) + expect(await fs.readFile(allowed, "utf8")).toBe("allowed") + expect(await fs.readFile(sentinel, "utf8")).toBe("original") + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("terminates daemonized descendants when the command scope closes", async () => { + const root = await fixture() + const ready = path.join(root.project, "ready") + const marker = path.join(root.project, "marker") + const child = [ + 'const fs = require("node:fs")', + `setInterval(() => fs.writeFileSync(${JSON.stringify(marker)}, String(Date.now())), 20)`, + ].join("\n") + const script = [ + 'const fs = require("node:fs")', + 'const child = require("node:child_process")', + `const proc = child.spawn(process.execPath, ["-e", ${JSON.stringify(child)}], { detached: true, stdio: "ignore" })`, + "proc.unref()", + `fs.writeFileSync(${JSON.stringify(ready)}, "ready")`, + "setInterval(() => {}, 10_000)", + ].join("\n") + + try { + await Effect.runPromise( + Effect.scoped( + run( + profile([root.project]), + ChildProcessSpawner.ChildProcessSpawner.use((spawner) => + Effect.gen(function* () { + yield* spawner.spawn(ChildProcess.make(process.execPath, ["-e", script], { cwd: root.project })) + yield* Effect.promise(async () => { + const deadline = Date.now() + 5_000 + while (Date.now() < deadline) { + const started = await Promise.all( + [ready, marker].map((file) => + fs.stat(file).then( + () => true, + () => false, + ), + ), + ) + if (started.every(Boolean)) return + await Bun.sleep(20) + } + throw new Error("daemonized child did not start") + }) + }), + ), + ).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)), + ), + ) + + await Bun.sleep(100) + const stopped = await fs.readFile(marker, "utf8") + await Bun.sleep(150) + expect(await fs.readFile(marker, "utf8")).toBe(stopped) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("rejects a Bubblewrap helper inside a writable root", async () => { + const root = await fixture() + const source = process.env.KILO_BWRAP_PATH ?? "/usr/bin/bwrap" + const helper = path.join(root.project, "bwrap") + const link = path.join(root.outside, "bwrap") + await fs.copyFile(source, helper) + await fs.chmod(helper, 0o755) + await fs.symlink(helper, link) + const script = [ + 'import { Effect } from "effect"', + 'import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"', + 'import { backendSupport, run } from "@kilocode/sandbox"', + 'import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"', + "if (!backendSupport().available) process.exit(2)", + `const profile = { filesystem: { allowWrite: [{ path: ${JSON.stringify(root.project)}, kind: "subtree" }], denyWrite: [], denyNames: [] }, network: { mode: "allow", allowedHosts: [] }, environment: { deny: [], set: {} } }`, + 'const effect = Effect.scoped(run(profile, ChildProcessSpawner.ChildProcessSpawner.use((spawner) => spawner.spawn(ChildProcess.make(process.execPath, ["-e", "process.exit(0)"])))).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)))', + "try { await Effect.runPromise(effect); process.exit(3) } catch { process.exit(0) }", + ].join("\n") + + try { + const result = spawnSync(process.execPath, ["-e", script], { + cwd: import.meta.dir, + env: { ...process.env, KILO_BWRAP_PATH: link }, + encoding: "utf8", + }) + expect(result.status, result.stderr).toBe(0) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("reports network namespace support separately and fails deny mode closed", async () => { + const root = await fixture() + const source = process.env.KILO_BWRAP_PATH ?? "/usr/bin/bwrap" + const helper = path.join(root.outside, "bwrap-no-network") + await fs.writeFile( + helper, + [ + "#!/bin/sh", + 'for arg in "$@"; do', + ' if [ "$arg" = "--unshare-net" ]; then echo "network namespaces blocked" >&2; exit 42; fi', + "done", + `exec ${JSON.stringify(source)} "$@"`, + "", + ].join("\n"), + ) + await fs.chmod(helper, 0o755) + const script = [ + 'import { Effect } from "effect"', + 'import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"', + 'import { backendSupport, run } from "@kilocode/sandbox"', + 'import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"', + 'const allow = backendSupport({ mode: "allow", allowedHosts: [] })', + 'const deny = backendSupport({ mode: "deny", allowedHosts: [] })', + "if (!allow.available) process.exit(2)", + 'if (deny.available || !deny.reason?.includes("Linux network sandbox")) process.exit(3)', + 'const profile = { filesystem: { allowWrite: [], denyWrite: [], denyNames: [] }, network: { mode: "deny", allowedHosts: [] }, environment: { deny: [], set: {} } }', + 'const effect = Effect.scoped(run(profile, ChildProcessSpawner.ChildProcessSpawner.use((spawner) => spawner.spawn(ChildProcess.make(process.execPath, ["-e", "process.exit(0)"])))).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)))', + "try { await Effect.runPromise(effect); process.exit(4) } catch { process.exit(0) }", + ].join("\n") + + try { + const result = spawnSync(process.execPath, ["-e", script], { + cwd: import.meta.dir, + env: { ...process.env, KILO_BWRAP_PATH: helper }, + encoding: "utf8", + }) + expect(result.status, result.stderr).toBe(0) + } finally { + await fs.rm(root.root, { recursive: true, force: true }) + } +}) + +linux("fails closed when Bubblewrap is unavailable", () => { + const script = [ + 'import { Effect } from "effect"', + 'import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"', + 'import { backendSupport, run } from "@kilocode/sandbox"', + 'import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"', + "if (backendSupport().available) process.exit(2)", + 'const profile = { filesystem: { allowWrite: [], denyWrite: [], denyNames: [] }, network: { mode: "allow", allowedHosts: [] }, environment: { deny: [], set: {} } }', + 'const effect = Effect.scoped(run(profile, ChildProcessSpawner.ChildProcessSpawner.use((spawner) => spawner.spawn(ChildProcess.make(process.execPath, ["-e", "process.exit(0)"])))).pipe(Effect.provide(CrossSpawnSpawner.defaultLayer)))', + "try { await Effect.runPromise(effect); process.exit(3) } catch { process.exit(0) }", + ].join("\n") + const result = spawnSync(process.execPath, ["-e", script], { + cwd: import.meta.dir, + env: { ...process.env, KILO_BWRAP_PATH: "/missing/kilo-bwrap" }, + encoding: "utf8", + }) + expect(result.status, result.stderr).toBe(0) +}) diff --git a/packages/core/test/kilocode/provider-isolation.test.ts b/packages/core/test/kilocode/provider-isolation.test.ts new file mode 100644 index 00000000000..ec33d4768a0 --- /dev/null +++ b/packages/core/test/kilocode/provider-isolation.test.ts @@ -0,0 +1,71 @@ +import { describe, expect } from "bun:test" +import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" +import { ModelV2 } from "@opencode-ai/core/model" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway" +import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia" +import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter" +import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel" +import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { it, model, provider } from "../plugin/provider-helper" + +describe("provider attribution isolation", () => { + it.effect("leaves custom providers with official endpoints untouched", () => + Effect.gen(function* () { + const plugins = yield* PluginV2.Service + const catalog = yield* Catalog.Service + for (const plugin of [LLMGatewayPlugin, NvidiaPlugin, OpenRouterPlugin, VercelPlugin, ZenmuxPlugin]) { + yield* plugins.add(plugin) + } + + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const items = [ + provider("custom-llmgateway", { + enabled: { via: "env", name: "CUSTOM_LLMGATEWAY_API_KEY" }, + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, + }), + provider("custom-nvidia", { + endpoint: { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://integrate.api.nvidia.com/v1", + }, + }), + provider("custom-openrouter", { + endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, + }), + provider("custom-vercel", { + endpoint: { type: "aisdk", package: "@ai-sdk/vercel" }, + }), + provider("custom-zenmux", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + }), + ] + + for (const item of items) { + catalog.provider.update(item.id, (draft) => { + draft.enabled = item.enabled + draft.endpoint = item.endpoint + draft.options.headers.Existing = "value" + }) + } + for (const id of ["gpt-5-chat-latest", "openai/gpt-5-chat"]) { + const item = model("custom-openrouter", id) + catalog.model.update(item.providerID, item.id, () => {}) + } + }) + + for (const id of ["custom-llmgateway", "custom-nvidia", "custom-openrouter", "custom-vercel", "custom-zenmux"]) { + expect((yield* catalog.provider.get(ProviderV2.ID.make(id))).options.headers).toEqual({ Existing: "value" }) + } + for (const id of ["gpt-5-chat-latest", "openai/gpt-5-chat"]) { + expect((yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make(id))).enabled).toBe( + true, + ) + } + }), + ) +}) diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts new file mode 100644 index 00000000000..305083bfedd --- /dev/null +++ b/packages/core/test/location.test.ts @@ -0,0 +1,38 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { Location } from "@opencode-ai/core/location" +import { Project } from "@opencode-ai/core/project" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { testEffect } from "./lib/effect" + +const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID: "workspace" } +const projectLayer = Layer.succeed( + Project.Service, + Project.Service.of({ + resolve: () => + Effect.succeed({ + id: Project.ID.make("project"), + directory: AbsolutePath.make("/repo"), + vcs: { type: "git", store: AbsolutePath.make("/repo/.git") }, + }), + commit: () => Effect.void, + }), +) +const it = testEffect(Location.layer(ref).pipe(Layer.provide(projectLayer))) + +describe("Location", () => { + it.effect("resolves the current project and vcs information", () => + Effect.gen(function* () { + const location = yield* Location.Service + + expect(location.directory).toBe(AbsolutePath.make("/repo/packages/app")) + expect(location.workspaceID).toBe("workspace") + expect(location.project.id).toBe(Project.ID.make("project")) + expect(location.project.directory).toBe(AbsolutePath.make("/repo")) + expect(location.vcs).toEqual({ + type: "git", + store: AbsolutePath.make("/repo/.git"), + }) + }), + ) +}) diff --git a/packages/core/test/model.test.ts b/packages/core/test/model.test.ts new file mode 100644 index 00000000000..fe97acc25aa --- /dev/null +++ b/packages/core/test/model.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" + +const decode = Schema.decodeUnknownSync(ModelV2.Ref) + +describe("ModelV2.Ref", () => { + test("accepts a model selection without a variant", () => { + expect(decode({ id: "claude-sonnet", providerID: "anthropic" })).toEqual({ + id: ModelV2.ID.make("claude-sonnet"), + providerID: ProviderV2.ID.make("anthropic"), + }) + }) + + test("preserves an explicit model variant", () => { + expect(decode({ id: "claude-sonnet", providerID: "anthropic", variant: "high" })).toEqual({ + id: ModelV2.ID.make("claude-sonnet"), + providerID: ProviderV2.ID.make("anthropic"), + variant: ModelV2.VariantID.make("high"), + }) + }) +}) diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 26e254f53e8..75e8f7f5afe 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -4,7 +4,8 @@ import { HttpClient, HttpClientResponse } from "effect/unstable/http" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@opencode-ai/core/global" -import { ModelsDev } from "@opencode-ai/core/models" +import { ModelsDev } from "@opencode-ai/core/models-dev" +import { EventV2 } from "@opencode-ai/core/event" import { it } from "./lib/effect" import { rm, writeFile, utimes, mkdir } from "fs/promises" import path from "path" @@ -92,6 +93,7 @@ const buildLayer = (state: Ref.Ref) => Layer.fresh(ModelsDev.layer).pipe( Layer.provide(Layer.succeed(HttpClient.HttpClient, makeMockClient(state))), Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(EventV2.defaultLayer), ) const writeCache = (data: object, mtimeMs?: number) => @@ -136,14 +138,14 @@ describe("ModelsDev Service", () => { }), ) - it.live("get() returns bundled snapshot when disk empty and fetch disabled", () => + it.live("get() returns empty catalog when disk empty, fetch disabled, and no bundled snapshot is injected", () => Effect.gen(function* () { const state = yield* Ref.make(initialState) const result = yield* provided( state, ModelsDev.Service.use((s) => s.get()), ) - expect(Object.keys(result).length).toBeGreaterThan(0) + expect(result).toEqual({}) const final = yield* Ref.get(state) expect(final.calls).toEqual([]) }), diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index c70ada08d94..602e85624b7 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -1,7 +1,9 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock" +import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") { @@ -20,27 +22,30 @@ describe("AmazonBedrockPlugin", () => { it.effect("moves endpoint option to endpoint URL", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AmazonBedrockPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("amazon-bedrock", { - options: { - headers: {}, - body: {}, - aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} }, - }, - }), - cancel: false, - }, - ) - expect(result.provider.endpoint).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const bedrock = provider("amazon-bedrock", { + endpoint: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, + options: { + headers: {}, + body: {}, + aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} }, + }, + }) + catalog.provider.update(bedrock.id, (item) => { + item.endpoint = bedrock.endpoint + item.options = bedrock.options + }) + }) + const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock) + expect(result.endpoint).toEqual({ type: "aisdk", - package: "test-provider", + package: "@ai-sdk/amazon-bedrock", url: "https://bedrock.example", }) - expect(result.provider.options.aisdk.provider.endpoint).toBeUndefined() + expect(result.options.aisdk.provider.endpoint).toBeUndefined() }), ) diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index bbea4a37215..6cae612fd22 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -1,37 +1,43 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic" +import { ProviderV2 } from "@opencode-ai/core/provider" import { it, model, provider } from "./provider-helper" describe("AnthropicPlugin", () => { it.effect("applies legacy beta headers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AnthropicPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("anthropic", { - options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - expect(result.provider.options.headers["anthropic-beta"]).toBe( + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("anthropic", { + endpoint: { type: "aisdk", package: "@ai-sdk/anthropic" }, + options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers["anthropic-beta"]).toBe( "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", ) - expect(result.provider.options.headers.Existing).toBe("1") + expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers.Existing).toBe("1") }), ) it.effect("ignores non-Anthropic providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AnthropicPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false }) - expect(result.provider.options.headers["anthropic-beta"]).toBeUndefined() + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {})) + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.headers["anthropic-beta"]).toBeUndefined() }), ) diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index b835cbeeff0..a3837a66ad9 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -1,7 +1,9 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure" +import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" describe("AzureCognitiveServicesPlugin", () => { @@ -9,20 +11,22 @@ describe("AzureCognitiveServicesPlugin", () => { withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: "cognitive" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzureCognitiveServicesPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("azure-cognitive-services"), cancel: false }, - ) - expect(result.provider.endpoint).toEqual({ - type: "aisdk", - package: "test-provider", + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { + item.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" } + }) }) - expect(result.provider.options.aisdk.provider.baseURL).toBe( - "https://cognitive.cognitiveservices.azure.com/openai", - ) - expect(result.provider.options.aisdk.provider.resourceName).toBeUndefined() + const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) + expect(result.endpoint).toEqual({ + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://cognitive.cognitiveservices.azure.com/openai", + }) + expect(result.options.aisdk.provider.baseURL).toBeUndefined() + expect(result.options.aisdk.provider.resourceName).toBeUndefined() }), ), ) @@ -31,17 +35,27 @@ describe("AzureCognitiveServicesPlugin", () => { withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzureCognitiveServicesPlugin) - const azure = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("azure-cognitive-services"), cancel: false }, - ) - const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false }) - expect(azure.provider.options.aisdk.provider.baseURL).toBeUndefined() - expect(azure.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" }) - expect(other.provider.options.aisdk.provider.baseURL).toBeUndefined() - expect(other.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" }) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const azure = provider("azure-cognitive-services", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, + }) + const openai = provider("openai") + catalog.provider.update(azure.id, (item) => { + item.endpoint = azure.endpoint + }) + catalog.provider.update(openai.id, (item) => { + item.endpoint = openai.endpoint + }) + }) + const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")) + const openai = yield* catalog.provider.get(ProviderV2.ID.openai) + expect(azure.options.aisdk.provider.baseURL).toBeUndefined() + expect(azure.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" }) + expect(openai.options.aisdk.provider.baseURL).toBeUndefined() + expect(openai.endpoint).toEqual({ type: "aisdk", package: "test-provider" }) }), ), ) diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 5121b1eec02..18670d69015 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -1,22 +1,46 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AuthV2 } from "@opencode-ai/core/auth" +import { AccountV2 } from "@opencode-ai/core/account" +import { Catalog } from "@opencode-ai/core/catalog" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" import { PluginV2 } from "@opencode-ai/core/plugin" -import { AuthPlugin } from "@opencode-ai/core/plugin/auth" +import { Policy } from "@opencode-ai/core/policy" +import { AccountPlugin } from "@opencode-ai/core/plugin/account" import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper" -const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer)) +const itWithAccount = testEffect( + Catalog.layer.pipe( + Layer.provideMerge(PluginV2.defaultLayer), + Layer.provideMerge(AccountV2.defaultLayer), + Layer.provideMerge(EventV2.defaultLayer), + Layer.provide(Policy.defaultLayer), + Layer.provideMerge( + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), + ), + Layer.provideMerge(npmLayer), + ), +) describe("AzurePlugin", () => { it.effect("resolves resourceName from env", () => withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzurePlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false }) - expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env") + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.azure, (item) => { + item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" } + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") }), ), ) @@ -25,25 +49,29 @@ describe("AzurePlugin", () => { withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzurePlugin) - const azure = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("azure", { - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const azure = provider("azure", { + endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, + options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } }, + }) + catalog.provider.update(azure.id, (item) => { + item.endpoint = azure.endpoint + item.options = azure.options + }) + catalog.provider.update(ProviderV2.ID.openai, () => {}) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe( + "from-config", ) - const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false }) - expect(azure.provider.options.aisdk.provider.resourceName).toBe("from-config") - expect(other.provider.options.aisdk.provider.resourceName).toBeUndefined() + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.resourceName).toBeUndefined() }), ), ) - itWithAuth.effect("prefers auth resourceName over env", () => + itWithAccount.effect("prefers account resourceName over env", () => withEnv( { AZURE_RESOURCE_NAME: "from-env", @@ -51,23 +79,36 @@ describe("AzurePlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service - const auth = yield* AuthV2.Service - yield* auth.create({ - serviceID: AuthV2.ServiceID.make("azure"), - credential: new AuthV2.ApiKeyCredential({ + const accounts = yield* AccountV2.Service + const catalog = yield* Catalog.Service + const events = yield* EventV2.Service + yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("azure"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "key", - metadata: { resourceName: "from-auth" }, + metadata: { resourceName: "from-account" }, }), - active: true, }) yield* plugin.add({ - ...AuthPlugin, - effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)), + ...AccountPlugin, + effect: AccountPlugin.effect.pipe( + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(EventV2.Service, events), + Effect.provideService(PluginV2.Service, plugin), + ), }) yield* plugin.add(AzurePlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false }) - expect(result.provider.options.aisdk.provider.resourceName).toBe("from-auth") + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.azure, (item) => { + item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" } + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe( + "from-account", + ) }), ), ) @@ -76,18 +117,20 @@ describe("AzurePlugin", () => { withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzurePlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("azure", { - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } }, - }), - cancel: false, - }, - ) - expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env") + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const azure = provider("azure", { + endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, + options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } }, + }) + catalog.provider.update(azure.id, (item) => { + item.endpoint = azure.endpoint + item.options = azure.options + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") }), ), ) @@ -96,18 +139,20 @@ describe("AzurePlugin", () => { withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(AzurePlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("azure", { - options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } }, - }), - cancel: false, - }, - ) - expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env") + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const azure = provider("azure", { + endpoint: { type: "aisdk", package: "@ai-sdk/azure" }, + options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } }, + }) + catalog.provider.update(azure.id, (item) => { + item.endpoint = azure.endpoint + item.options = azure.options + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env") }), ), ) diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 7270d5367ae..982b587a71b 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -1,8 +1,10 @@ import { describe, expect, mock } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras" -import { it, model, provider } from "./provider-helper" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { it, model } from "./provider-helper" const cerebrasOptions: Record[] = [] @@ -20,27 +22,30 @@ describe("CerebrasPlugin", () => { it.effect("applies the legacy integration header", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(CerebrasPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("cerebras", { - options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - expect(result.provider.options.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode" }) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { + item.endpoint = { type: "aisdk", package: "@ai-sdk/cerebras" } + item.options.headers.Existing = "1" + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).options.headers).toEqual({ + Existing: "1", + "X-Cerebras-3rd-Party-Integration": "opencode", + }) }), ) it.effect("ignores non-Cerebras providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(CerebrasPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("groq"), cancel: false }) - expect(result.provider.options.headers).toEqual({}) + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) + expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).options.headers).toEqual({}) }), ) diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 10aba171c33..e6c66185990 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,14 +1,32 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AuthV2 } from "@opencode-ai/core/auth" +import { AccountV2 } from "@opencode-ai/core/account" +import { Catalog } from "@opencode-ai/core/catalog" +import { Location } from "@opencode-ai/core/location" +import { EventV2 } from "@opencode-ai/core/event" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" -import { AuthPlugin } from "@opencode-ai/core/plugin/auth" +import { Policy } from "@opencode-ai/core/policy" +import { AccountPlugin } from "@opencode-ai/core/plugin/account" import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" import { testEffect } from "../lib/effect" -import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper" +import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper" -const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer)) +const itWithAccount = testEffect( + Catalog.layer.pipe( + Layer.provideMerge(PluginV2.defaultLayer), + Layer.provideMerge(AccountV2.defaultLayer), + Layer.provideMerge(EventV2.defaultLayer), + Layer.provide(Policy.defaultLayer), + Layer.provideMerge( + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), + ), + Layer.provideMerge(npmLayer), + ), +) function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") { return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel( @@ -34,22 +52,25 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(CloudflareWorkersAIPlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("cloudflare-workers-ai"), cancel: false }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { + provider.endpoint = { type: "aisdk", package: "test-provider" } + }), ) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")) const sdk = yield* plugin.trigger( "aisdk.sdk", { - model: model("cloudflare-workers-ai", "@cf/model", { endpoint: updated.provider.endpoint }), + model: model("cloudflare-workers-ai", "@cf/model", { endpoint: provider.endpoint }), package: "@ai-sdk/openai-compatible", options: { name: "cloudflare-workers-ai", headers: { custom: "header" } }, }, {}, ) - expect(updated.provider.endpoint).toEqual({ + expect(provider.endpoint).toEqual({ type: "aisdk", package: "test-provider", url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1", @@ -63,18 +84,15 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(CloudflareWorkersAIPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("cloudflare-workers-ai", { - endpoint: { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { + provider.endpoint = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" } + }), ) - expect(result.provider.endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ type: "aisdk", package: "test-provider", url: "https://proxy.example/v1", @@ -104,7 +122,7 @@ describe("CloudflareWorkersAIPlugin", () => { ), ) - itWithAuth.effect("falls back to auth account metadata when account env is absent", () => + itWithAccount.effect("falls back to account metadata when account env is absent", () => withEnv( { CLOUDFLARE_ACCOUNT_ID: undefined, @@ -113,30 +131,37 @@ describe("CloudflareWorkersAIPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service - const auth = yield* AuthV2.Service - yield* auth.create({ - serviceID: AuthV2.ServiceID.make("cloudflare-workers-ai"), - credential: new AuthV2.ApiKeyCredential({ + const accounts = yield* AccountV2.Service + const catalog = yield* Catalog.Service + const events = yield* EventV2.Service + yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("cloudflare-workers-ai"), + credential: new AccountV2.ApiKeyCredential({ type: "api", - key: "auth-key", - metadata: { accountId: "auth-acct" }, + key: "account-key", + metadata: { accountId: "account-acct" }, }), - active: true, }) yield* plugin.add({ - ...AuthPlugin, - effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)), + ...AccountPlugin, + effect: AccountPlugin.effect.pipe( + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(EventV2.Service, events), + Effect.provideService(PluginV2.Service, plugin), + ), }) yield* plugin.add(CloudflareWorkersAIPlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("cloudflare-workers-ai"), cancel: false }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { + provider.endpoint = { type: "aisdk", package: "test-provider" } + }), ) - expect(updated.provider.endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ type: "aisdk", package: "test-provider", - url: "https://api.cloudflare.com/client/v4/accounts/auth-acct/ai/v1", + url: "https://api.cloudflare.com/client/v4/accounts/account-acct/ai/v1", }) }), ), @@ -146,18 +171,16 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(CloudflareWorkersAIPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("cloudflare-workers-ai", { - options: { headers: {}, body: {}, aisdk: { provider: { accountId: "configured-acct" }, request: {} } }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { + provider.endpoint = { type: "aisdk", package: "test-provider" } + provider.options.aisdk.provider.accountId = "configured-acct" + }), ) - expect(result.provider.endpoint).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({ type: "aisdk", package: "test-provider", url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1", diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index e2bd899ff3f..c07f70597ab 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -1,8 +1,10 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot" +import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model } from "./provider-helper" describe("GithubCopilotPlugin", () => { @@ -145,29 +147,35 @@ describe("GithubCopilotPlugin", () => { }), ) - it.effect("filters gpt-5-chat-latest before Copilot language selection", () => + it.effect("disables gpt-5-chat-latest before Copilot language selection", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GithubCopilotPlugin) - const result = yield* plugin.trigger( - "model.update", - {}, - { model: model("github-copilot", "gpt-5-chat-latest"), cancel: false }, - ) - expect(result.cancel).toBe(true) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {}) + catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + }) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled, + ).toBe(false) }), ) - it.effect("does not filter gpt-5-chat-latest for non-Copilot providers", () => + it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GithubCopilotPlugin) - const result = yield* plugin.trigger( - "model.update", - {}, - { model: model("custom-copilot", "gpt-5-chat-latest"), cancel: false }, - ) - expect(result.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {}) + catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + }) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled, + ).toBe(true) }), ) diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index 56a22649a99..f9efa294f5f 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -1,11 +1,17 @@ import { describe, expect, mock } from "bun:test" import { Effect, Layer } from "effect" -import { AuthV2 } from "@opencode-ai/core/auth" +import { AccountV2 } from "@opencode-ai/core/account" +import { Catalog } from "@opencode-ai/core/catalog" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" import { PluginV2 } from "@opencode-ai/core/plugin" -import { AuthPlugin } from "@opencode-ai/core/plugin/auth" +import { Policy } from "@opencode-ai/core/policy" +import { AccountPlugin } from "@opencode-ai/core/plugin/account" import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" import { testEffect } from "../lib/effect" -import { it, model, npmLayer, provider, withEnv } from "./provider-helper" +import { it, model, npmLayer, withEnv } from "./provider-helper" const gitlabSDKOptions: Record[] = [] @@ -22,7 +28,18 @@ void mock.module("gitlab-ai-provider", () => ({ isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact", })) -const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer)) +const itWithAccount = testEffect( + Layer.mergeAll( + Catalog.defaultLayer, + PluginV2.defaultLayer, + AccountV2.defaultLayer, + EventV2.defaultLayer, + npmLayer, + ).pipe( + Layer.provide(Policy.defaultLayer), + Layer.provide(Location.defaultLayer({ directory: AbsolutePath.make("/") })), + ), +) describe("GitLabPlugin", () => { it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () => @@ -141,7 +158,7 @@ describe("GitLabPlugin", () => { }), ) - itWithAuth.effect("uses active API auth token over GITLAB_TOKEN", () => + itWithAccount.effect("uses active account API token over GITLAB_TOKEN", () => withEnv( { GITLAB_TOKEN: "env-token", @@ -150,33 +167,41 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service - const auth = yield* AuthV2.Service - yield* auth.create({ - serviceID: AuthV2.ServiceID.make("gitlab"), - credential: new AuthV2.ApiKeyCredential({ type: "api", key: "auth-token" }), - active: true, + const accounts = yield* AccountV2.Service + const catalog = yield* Catalog.Service + const events = yield* EventV2.Service + yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("gitlab"), + credential: new AccountV2.ApiKeyCredential({ type: "api", key: "account-token" }), }) yield* plugin.add({ - ...AuthPlugin, - effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)), + ...AccountPlugin, + effect: AccountPlugin.effect.pipe( + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(EventV2.Service, events), + Effect.provideService(PluginV2.Service, plugin), + ), }) yield* plugin.add(GitLabPlugin) - const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("gitlab"), cancel: false }) + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {})) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab")) yield* plugin.trigger( "aisdk.sdk", { model: model("gitlab", "claude"), package: "gitlab-ai-provider", - options: updated.provider.options.aisdk.provider, + options: provider.options.aisdk.provider, }, {}, ) - expect(gitlabSDKOptions[0].apiKey).toBe("auth-token") + expect(gitlabSDKOptions[0].apiKey).toBe("account-token") }), ), ) - itWithAuth.effect("uses active OAuth access token when no API auth exists", () => + itWithAccount.effect("uses active account OAuth access token when no API token exists", () => withEnv( { GITLAB_TOKEN: undefined, @@ -185,33 +210,41 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service - const auth = yield* AuthV2.Service - yield* auth.create({ - serviceID: AuthV2.ServiceID.make("gitlab"), - credential: new AuthV2.OAuthCredential({ + const accounts = yield* AccountV2.Service + const catalog = yield* Catalog.Service + const events = yield* EventV2.Service + yield* accounts.create({ + serviceID: AccountV2.ServiceID.make("gitlab"), + credential: new AccountV2.OAuthCredential({ type: "oauth", refresh: "refresh-token", - access: "oauth-token", + access: "account-oauth-token", expires: 9999999999999, }), - active: true, }) yield* plugin.add({ - ...AuthPlugin, - effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)), + ...AccountPlugin, + effect: AccountPlugin.effect.pipe( + Effect.provideService(AccountV2.Service, accounts), + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(EventV2.Service, events), + Effect.provideService(PluginV2.Service, plugin), + ), }) yield* plugin.add(GitLabPlugin) - const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("gitlab"), cancel: false }) + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {})) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab")) yield* plugin.trigger( "aisdk.sdk", { model: model("gitlab", "claude"), package: "gitlab-ai-provider", - options: updated.provider.options.aisdk.provider, + options: provider.options.aisdk.provider, }, {}, ) - expect(gitlabSDKOptions[0].apiKey).toBe("oauth-token") + expect(gitlabSDKOptions[0].apiKey).toBe("account-oauth-token") }), ), ) diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 6bcece53c95..9d23dfefb35 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -1,8 +1,10 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" -import { GoogleVertexAnthropicPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper" describe("GoogleVertexAnthropicPlugin", () => { it.effect("resolves legacy project and location env on provider update", () => @@ -18,14 +20,17 @@ describe("GoogleVertexAnthropicPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("google-vertex-anthropic"), cancel: false }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { + provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } + }), ) - expect(result.provider.options.aisdk.provider.project).toBe("cloud-project") - expect(result.provider.options.aisdk.provider.location).toBe("cloud-location") + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")) + expect(provider.options.aisdk.provider.project).toBe("cloud-project") + expect(provider.options.aisdk.provider.location).toBe("cloud-location") }), ), ) @@ -34,23 +39,19 @@ describe("GoogleVertexAnthropicPlugin", () => { withEnv({ GOOGLE_CLOUD_PROJECT: "env-project", GOOGLE_CLOUD_LOCATION: "env-location" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("google-vertex-anthropic", { - options: { - headers: {}, - body: {}, - aisdk: { provider: { project: "configured-project", location: "configured-location" }, request: {} }, - }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { + provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } + provider.options.aisdk.provider.project = "configured-project" + provider.options.aisdk.provider.location = "configured-location" + }), ) - expect(result.provider.options.aisdk.provider.project).toBe("configured-project") - expect(result.provider.options.aisdk.provider.location).toBe("configured-location") + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")) + expect(provider.options.aisdk.provider.project).toBe("configured-project") + expect(provider.options.aisdk.provider.location).toBe("configured-location") }), ), ) @@ -108,6 +109,73 @@ describe("GoogleVertexAnthropicPlugin", () => { ), ) + it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models", + ) + }), + ) + + it.effect("keeps configured baseURL for google-vertex Anthropic models", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1") + }), + ) + + it.effect("selects google-vertex Anthropic language models through V2 plugins", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexPlugin) + yield* plugin.add(GoogleVertexAnthropicPlugin) + const sdkResult = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "us" }, + }, + {}, + ) + const languageResult = yield* plugin.trigger( + "aisdk.language", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + sdk: sdkResult.sdk, + options: {}, + }, + {}, + ) + const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string } + expect(language.config.baseURL).toBe( + "https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models", + ) + expect(language.modelId).toBe("claude-sonnet-4-5") + }), + ) + it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 3bd60fd7211..2abb342cc38 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -1,10 +1,13 @@ import { describe, expect, mock } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper" const vertexOptions: Record[] = [] +const googleAuthOptions: Record[] = [] void mock.module("@ai-sdk/google-vertex", () => ({ createVertex: (options: Record) => { @@ -17,12 +20,14 @@ void mock.module("@ai-sdk/google-vertex", () => ({ void mock.module("google-auth-library", () => ({ GoogleAuth: class { - async getApplicationDefault() { + constructor(options: Record) { + googleAuthOptions.push(options) + } + + async getClient() { return { - credential: { - async getAccessToken() { - return { token: "vertex-token" } - }, + async getAccessToken() { + return { token: "vertex-token" } }, } } @@ -43,24 +48,22 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("google-vertex", { - endpoint: { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", - }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + }), ) - expect(result.provider.options.aisdk.provider.project).toBe("google-cloud-project") - expect(result.provider.options.aisdk.provider.location).toBe("google-vertex-location") - expect(result.provider.endpoint).toEqual({ + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.options.aisdk.provider.project).toBe("google-cloud-project") + expect(provider.options.aisdk.provider.location).toBe("google-vertex-location") + expect(provider.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location", @@ -84,21 +87,19 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { vertexOptions.length = 0 const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexPlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("google-vertex", { - endpoint: { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", - }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + }), ) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) yield* plugin.trigger( "aisdk.sdk", { @@ -111,8 +112,8 @@ describe("GoogleVertexPlugin", () => { {}, ) - expect(updated.provider.options.aisdk.provider.project).toBe("vertex-project") - expect(updated.provider.endpoint).toEqual({ + expect(provider.options.aisdk.provider.project).toBe("vertex-project") + expect(provider.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4", @@ -136,29 +137,24 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("google-vertex", { - endpoint: { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", - }, - options: { - headers: {}, - body: {}, - aisdk: { provider: { project: "config-project", location: "global" }, request: {} }, - }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + provider.options.aisdk.provider.project = "config-project" + provider.options.aisdk.provider.location = "global" + }), ) - expect(result.provider.options.aisdk.provider.project).toBe("config-project") - expect(result.provider.options.aisdk.provider.location).toBe("global") - expect(result.provider.endpoint).toEqual({ + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.options.aisdk.provider.project).toBe("config-project") + expect(provider.options.aisdk.provider.location).toBe("global") + expect(provider.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global", @@ -167,6 +163,32 @@ describe("GoogleVertexPlugin", () => { ), ) + it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service + yield* plugin.add(GoogleVertexPlugin) + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + provider.options.aisdk.provider.project = "config-project" + provider.options.aisdk.provider.location = "eu" + }), + ) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.endpoint).toEqual({ + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu", + }) + }), + ) + it.effect("defaults location to us-central1 when only project is configured", () => withEnv( { @@ -180,19 +202,18 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(GoogleVertexPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("google-vertex", { - options: { headers: {}, body: {}, aisdk: { provider: { project: "config-project" }, request: {} } }, - }), - cancel: false, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex" } + provider.options.aisdk.provider.project = "config-project" + }), ) - expect(result.provider.options.aisdk.provider.project).toBe("config-project") - expect(result.provider.options.aisdk.provider.location).toBe("us-central1") + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.options.aisdk.provider.project).toBe("config-project") + expect(provider.options.aisdk.provider.location).toBe("us-central1") }), ), ) @@ -229,6 +250,7 @@ describe("GoogleVertexPlugin", () => { it.effect("keeps Google auth fetch for OpenAI-compatible Vertex endpoints", () => Effect.gen(function* () { + googleAuthOptions.length = 0 const fetchCalls: { input: Parameters[0]; init?: RequestInit }[] = [] const plugin = yield* PluginV2.Service yield* plugin.add(GoogleVertexPlugin) @@ -274,6 +296,7 @@ describe("GoogleVertexPlugin", () => { }), ) expect(fetchCalls).toHaveLength(1) + expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }]) expect(fetchCalls[0].input).toBe("https://vertex.example") expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token") expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1") diff --git a/packages/core/test/plugin/provider-helper.ts b/packages/core/test/plugin/provider-helper.ts index 0d67075ac8e..a6d25ac187a 100644 --- a/packages/core/test/plugin/provider-helper.ts +++ b/packages/core/test/plugin/provider-helper.ts @@ -2,12 +2,22 @@ import { Npm } from "@opencode-ai/core/npm" import type { LanguageModelV3 } from "@ai-sdk/provider" import { expect } from "bun:test" import { Effect, Layer, Option } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" +import { Policy } from "@opencode-ai/core/policy" import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" import { testEffect } from "../lib/effect" export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make("test") })), +) export const npmLayer = Layer.succeed( Npm.Service, @@ -18,7 +28,34 @@ export const npmLayer = Layer.succeed( }), ) -export const it = testEffect(Layer.mergeAll(PluginV2.defaultLayer, npmLayer)) +export const catalogLayer = Layer.succeed( + Catalog.Service, + Catalog.Service.of({ + transform: () => Effect.die("unexpected catalog.transform"), + provider: { + get: () => Effect.die("unexpected provider.get"), + all: () => Effect.succeed([]), + available: () => Effect.succeed([]), + }, + model: { + get: () => Effect.die("unexpected model.get"), + all: () => Effect.succeed([]), + available: () => Effect.succeed([]), + default: () => Effect.succeed(Option.none()), + small: () => Effect.succeed(Option.none()), + }, + }), +) + +export const it = testEffect( + Catalog.layer.pipe( + Layer.provideMerge(PluginV2.defaultLayer), + Layer.provideMerge(EventV2.defaultLayer), + Layer.provide(Policy.defaultLayer), + Layer.provideMerge(locationLayer), + Layer.provideMerge(npmLayer), + ), +) export function provider(providerID: string, options?: Partial) { return new ProviderV2.Info({ diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index 33b2593bf9b..ff80ecd3d5d 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -1,9 +1,11 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo" -import { expectPluginRegistered, it, model, provider, withEnv } from "./provider-helper" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { expectPluginRegistered, it, model, provider, withEnv } from "./provider-helper" // kilocode_change describe("KiloPlugin", () => { it.effect("is registered so legacy referer headers can be applied", () => @@ -18,110 +20,121 @@ describe("KiloPlugin", () => { it.effect("applies legacy referer headers only to kilo", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("kilo", { - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("openrouter"), cancel: false }) - expect(result.provider.options.headers).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const kilo = provider("kilo", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(kilo.id, (draft) => { + draft.endpoint = kilo.endpoint + draft.options = kilo.options + }) + catalog.provider.update(provider("openrouter").id, () => {}) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://kilo.ai/", - "X-Title": "Kilo Code", + "X-Title": "Kilo Code", // kilocode_change }) - expect(ignored.provider.options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) }), ) it.effect("uses the exact legacy Kilo header casing and set", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("kilo"), cancel: false }) - - expect(result.provider.options.headers).toEqual({ - "HTTP-Referer": "https://kilo.ai/", - "X-Title": "Kilo Code", + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("kilo", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + }) }) - expect(result.provider.options.headers).not.toHaveProperty("http-referer") - expect(result.provider.options.headers).not.toHaveProperty("x-title") - expect(result.provider.options.headers).not.toHaveProperty("X-Source") + + const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo")) + expect(result.options.headers).toEqual({ + "HTTP-Referer": "https://kilo.ai/", + "X-Title": "Kilo Code", // kilocode_change + }) + expect(result.options.headers).not.toHaveProperty("http-referer") + expect(result.options.headers).not.toHaveProperty("x-title") + expect(result.options.headers).not.toHaveProperty("X-Source") }), ) it.effect("uses the legacy provider-id guard instead of endpoint package matching", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const matchingID = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("kilo", { - endpoint: { type: "aisdk", package: "not-kilo" }, - }), - cancel: false, - }, - ) - const matchingPackage = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("custom-kilo", { - endpoint: { type: "aisdk", package: "kilo" }, - }), - cancel: false, - }, - ) - - expect(matchingID.provider.options.headers).toEqual({ - "HTTP-Referer": "https://kilo.ai/", - "X-Title": "Kilo Code", + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const kilo = provider("kilo", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + }) + catalog.provider.update(kilo.id, (draft) => { + draft.endpoint = kilo.endpoint + }) + const custom = provider("custom-kilo", { + endpoint: { type: "aisdk", package: "kilo" }, + }) + catalog.provider.update(custom.id, (draft) => { + draft.endpoint = custom.endpoint + }) }) - expect(matchingPackage.provider.options.headers).toEqual({}) + + expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({ + "HTTP-Referer": "https://kilo.ai/", + "X-Title": "Kilo Code", // kilocode_change + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).options.headers).toEqual({}) }), ) + // kilocode_change start it.effect("routes the Kilo catalog through the Kilo Gateway SDK", () => withEnv({ KILO_API_KEY: undefined, KILO_ORG_ID: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("kilo", { - endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, - options: { - headers: {}, - body: {}, - aisdk: { provider: { apiKey: "stored-token" }, request: {} }, - }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("kilo", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" }, + options: { + headers: {}, + body: {}, + aisdk: { provider: { apiKey: "stored-token" }, request: {} }, + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) + const updated = yield* catalog.provider.get(ProviderV2.ID.make("kilo")) - expect(updated.provider.endpoint).toEqual({ + expect(updated.endpoint).toEqual({ type: "aisdk", package: "@kilocode/kilo-gateway", url: "https://api.kilo.ai/api/openrouter", }) - expect(updated.provider.options.aisdk.provider.kilocodeToken).toBe("stored-token") + expect(updated.options.aisdk.provider.kilocodeToken).toBe("stored-token") const result = yield* plugin.trigger( "aisdk.sdk", { model: model("kilo", "kilo-auto/free"), package: "@kilocode/kilo-gateway", - options: updated.provider.options.aisdk.provider, + options: updated.options.aisdk.provider, }, {}, ) @@ -136,29 +149,31 @@ describe("KiloPlugin", () => { withEnv({ KILO_API_KEY: "environment-token", KILO_ORG_ID: "environment-org" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("kilo", { - enabled: { via: "auth", service: "kilo" }, - options: { - headers: {}, - body: {}, - aisdk: { - provider: { apiKey: "authenticated-token", kilocodeOrganizationId: "authenticated-org" }, - request: {}, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("kilo", { + enabled: { via: "account", service: "kilo" }, + options: { + headers: {}, + body: {}, + aisdk: { + provider: { apiKey: "authenticated-token", kilocodeOrganizationId: "authenticated-org" }, + request: {}, }, - }), - cancel: false, - }, - ) + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.enabled = item.enabled + draft.options = item.options + }) + }) + const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo")) - expect(result.provider.enabled).toEqual({ via: "auth", service: "kilo" }) - expect(result.provider.options.aisdk.provider.kilocodeToken).toBe("authenticated-token") - expect(result.provider.options.aisdk.provider.kilocodeOrganizationId).toBe("environment-org") + expect(result.enabled).toEqual({ via: "account", service: "kilo" }) + expect(result.options.aisdk.provider.kilocodeToken).toBe("authenticated-token") + expect(result.options.aisdk.provider.kilocodeOrganizationId).toBe("environment-org") }), ), ) @@ -167,12 +182,16 @@ describe("KiloPlugin", () => { withEnv({ KILO_API_KEY: undefined, KILO_ORG_ID: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(KiloPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("kilo"), cancel: false }) + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("kilo"), () => {})) + const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo")) - expect(result.provider.enabled).toEqual({ via: "custom", data: { anonymous: true } }) - expect(result.provider.options.aisdk.provider.kilocodeToken).toBe("anonymous") + expect(result.enabled).toEqual({ via: "custom", data: { anonymous: true } }) + expect(result.options.aisdk.provider.kilocodeToken).toBe("anonymous") }), ), ) + // kilocode_change end }) diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 4c69aee7864..3e7402d0901 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -1,8 +1,10 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway" +import { ProviderV2 } from "@opencode-ai/core/provider" import { expectPluginRegistered, it, provider } from "./provider-helper" describe("LLMGatewayPlugin", () => { @@ -18,46 +20,54 @@ describe("LLMGatewayPlugin", () => { it.effect("applies legacy referer headers only to enabled llmgateway", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(LLMGatewayPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("llmgateway", { - enabled: { via: "env", name: "LLMGATEWAY_API_KEY" }, - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - const ignored = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("openrouter", { - enabled: { via: "env", name: "OPENROUTER_API_KEY" }, - }), - cancel: false, - }, - ) - expect(result.provider.options.headers).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const llmgateway = provider("llmgateway", { + enabled: { via: "env", name: "LLMGATEWAY_API_KEY" }, + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, + options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(llmgateway.id, (draft) => { + draft.enabled = llmgateway.enabled + draft.endpoint = llmgateway.endpoint + draft.options = llmgateway.options + }) + const openrouter = provider("openrouter", { + enabled: { via: "env", name: "OPENROUTER_API_KEY" }, + }) + catalog.provider.update(openrouter.id, (draft) => { + draft.enabled = openrouter.enabled + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", "X-Source": "kilo", }) - expect(ignored.provider.options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) }), ) it.effect("does not apply legacy headers to a disabled llmgateway provider", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(LLMGatewayPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("llmgateway"), cancel: false }) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("llmgateway", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + }) + }) - expect(result.provider.enabled).toBe(false) - expect(result.provider.options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).enabled).toBe(false) + expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({}) }), ) }) diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index 0ce3d370cba..aa55eb031e3 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -1,8 +1,10 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia" +import { ProviderV2 } from "@opencode-ai/core/provider" import { expectPluginRegistered, it, provider } from "./provider-helper" describe("NvidiaPlugin", () => { @@ -18,45 +20,48 @@ describe("NvidiaPlugin", () => { it.effect("applies NVIDIA tracking headers only to nvidia", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(NvidiaPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("nvidia", { - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("openrouter"), cancel: false }) - expect(result.provider.options.headers).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const nvidia = provider("nvidia", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(nvidia.id, (draft) => { + draft.endpoint = nvidia.endpoint + draft.options = nvidia.options + }) + catalog.provider.update(provider("openrouter").id, () => {}) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", "X-BILLING-INVOKE-ORIGIN": "KiloCode", }) - expect(ignored.provider.options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({}) }), ) it.effect("adds billing origin for custom NVIDIA endpoints", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(NvidiaPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("nvidia", { - endpoint: { type: "aisdk", package: "test-provider", url: "http://localhost:8000/v1" }, - options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("nvidia", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) - expect(result.provider.options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", "X-BILLING-INVOKE-ORIGIN": "KiloCode", @@ -67,23 +72,25 @@ describe("NvidiaPlugin", () => { it.effect("preserves an explicit NVIDIA billing origin header", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(NvidiaPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("nvidia", { - options: { - headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }, - body: {}, - aisdk: { provider: { baseURL: "https://integrate.api.nvidia.com/v1" }, request: {} }, - }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("nvidia", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" }, + options: { + headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }, + body: {}, + aisdk: { provider: { baseURL: "https://integrate.api.nvidia.com/v1" }, request: {} }, + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) - expect(result.provider.options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({ "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", "X-BILLING-INVOKE-ORIGIN": "CustomOrigin", diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index 31d6dd0b6d1..b65beb3c712 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,9 +1,11 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" -import { fakeSelectorSdk, it, model } from "./provider-helper" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { fakeSelectorSdk, it, model, provider } from "./provider-helper" describe("OpenAIPlugin", () => { it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => @@ -70,31 +72,39 @@ describe("OpenAIPlugin", () => { }), ) - it.effect("cancels gpt-5-chat-latest during model updates", () => + it.effect("disables gpt-5-chat-latest during catalog transforms", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpenAIPlugin) - const normal = yield* plugin.trigger("model.update", {}, { model: model("openai", "gpt-5"), cancel: false }) - const filtered = yield* plugin.trigger( - "model.update", - {}, - { model: model("openai", "gpt-5-chat-latest"), cancel: false }, - ) - expect(normal.cancel).toBe(false) - expect(filtered.cancel).toBe(true) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("openai", { endpoint: { type: "aisdk", package: "@ai-sdk/openai" } }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + }) + catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {}) + catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + }) + expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true) + expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false) }), ) - it.effect("does not cancel gpt-5-chat-latest for non-OpenAI providers", () => + it.effect("does not disable gpt-5-chat-latest for non-OpenAI providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpenAIPlugin) - const result = yield* plugin.trigger( - "model.update", - {}, - { model: model("custom-openai", "gpt-5-chat-latest"), cancel: false }, - ) - expect(result.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("custom-openai") + catalog.provider.update(item.id, () => {}) + catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + }) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled, + ).toBe(true) }), ) }) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index ed82686a21c..4f4dd6abef1 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -5,26 +5,36 @@ import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" +import { Policy } from "@opencode-ai/core/policy" import { ProviderV2 } from "@opencode-ai/core/provider" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "../fixture/location" import { it, model, provider, withEnv } from "./provider-helper" const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }] -const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" })) +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make("test") })), +) describe("OpencodePlugin", () => { - it.effect("uses a public key and cancels paid models without credentials", () => + it.effect("uses a public key and disables paid models without credentials", () => withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false }) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBe("public") - expect(paid.cancel).toBe(true) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode") + catalog.provider.update(item.id, () => {}) + const paid = model("opencode", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false) }), ), ) @@ -33,14 +43,19 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false }) - const free = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "free", { cost: cost(0) }), cancel: false }, - ) - expect(free.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode") + catalog.provider.update(item.id, () => {}) + const free = model("opencode", "free", { cost: cost(0) }) + catalog.model.update(item.id, free.id, (draft) => { + draft.cost = [...free.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true) }), ), ) @@ -49,14 +64,19 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false }) - const outputOnly = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "output-only", { cost: cost(0, 1) }), cancel: false }, - ) - expect(outputOnly.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode") + catalog.provider.update(item.id, () => {}) + const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) }) + catalog.model.update(item.id, outputOnly.id, (draft) => { + draft.cost = [...outputOnly.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public") + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(true) }), ), ) @@ -65,15 +85,19 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: "secret" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false }) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined() - expect(paid.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode") + catalog.provider.update(item.id, () => {}) + const paid = model("opencode", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -82,19 +106,21 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] }), cancel: false }, - ) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined() - expect(paid.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] }) + catalog.provider.update(item.id, (draft) => { + draft.env = [...item.env] + }) + const paid = model("opencode", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -103,31 +129,30 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("opencode", { - options: { - headers: {}, - body: {}, - aisdk: { - provider: { apiKey: "configured" }, - request: {}, - }, + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode", { + options: { + headers: {}, + body: {}, + aisdk: { + provider: { apiKey: "configured" }, + request: {}, }, - }), - cancel: false, - }, - ) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBe("configured") - expect(paid.cancel).toBe(false) + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.options = item.options + }) + const paid = model("opencode", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("configured") + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -136,19 +161,21 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger( - "provider.update", - {}, - { provider: provider("opencode", { enabled: { via: "auth", service: "opencode" } }), cancel: false }, - ) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("opencode", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined() - expect(paid.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("opencode", { enabled: { via: "account", service: "opencode" } }) + catalog.provider.update(item.id, (draft) => { + draft.enabled = item.enabled + }) + const paid = model("opencode", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -157,15 +184,19 @@ describe("OpencodePlugin", () => { withEnv({ OPENCODE_API_KEY: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpencodePlugin) - const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false }) - const paid = yield* plugin.trigger( - "model.update", - {}, - { model: model("openai", "paid", { cost: cost(1) }), cancel: false }, - ) - expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined() - expect(paid.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("openai") + catalog.provider.update(item.id, () => {}) + const paid = model("openai", "paid", { cost: cost(1) }) + catalog.model.update(item.id, paid.id, (draft) => { + draft.cost = [...paid.cost] + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.apiKey).toBeUndefined() + expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -175,23 +206,28 @@ describe("OpencodePlugin", () => { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.opencode - yield* catalog.provider.update(providerID, () => {}) - yield* catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => { - model.capabilities.input = ["text"] - model.capabilities.output = ["text"] - model.cost = cost(1, 1) - model.time.released = DateTime.makeUnsafe(Date.now()) - }) - yield* catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => { - model.capabilities.input = ["text"] - model.capabilities.output = ["text"] - model.cost = cost(10, 10) - model.time.released = DateTime.makeUnsafe(Date.now()) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(providerID, () => {}) + catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => { + model.capabilities.input = ["text"] + model.capabilities.output = ["text"] + model.cost = [...cost(1, 1)] + model.time.released = DateTime.makeUnsafe(Date.now()) + }) + catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => { + model.capabilities.input = ["text"] + model.capabilities.output = ["text"] + model.cost = [...cost(10, 10)] + model.time.released = DateTime.makeUnsafe(Date.now()) + }) }) const selected = yield* catalog.model.small(providerID) expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano")) - }).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(locationLayer)))), + }).pipe( + Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(Policy.defaultLayer), Layer.provide(locationLayer))), + ), ) }) diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 91db253aa01..c7b033eef78 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -1,8 +1,11 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" +import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter" +import { ProviderV2 } from "@opencode-ai/core/provider" import { expectPluginRegistered, it, model, provider } from "./provider-helper" describe("OpenRouterPlugin", () => { @@ -18,24 +21,27 @@ describe("OpenRouterPlugin", () => { it.effect("applies legacy referer headers only to openrouter", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpenRouterPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("openrouter", { - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("nvidia"), cancel: false }) - expect(result.provider.options.headers).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const openrouter = provider("openrouter", { + endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, + options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(openrouter.id, (item) => { + item.endpoint = openrouter.endpoint + item.options = openrouter.options + }) + catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {}) + }) + + expect((yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).options.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", }) - expect(ignored.provider.options.headers).toEqual({}) + expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({}) }), ) @@ -67,39 +73,50 @@ describe("OpenRouterPlugin", () => { it.effect("filters OpenRouter's gpt-5 chat alias", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpenRouterPlugin) - const result = yield* plugin.trigger( - "model.update", - {}, - { model: model("openrouter", "openai/gpt-5-chat"), cancel: false }, - ) - const regular = yield* plugin.trigger( - "model.update", - {}, - { model: model("openrouter", "openai/gpt-5"), cancel: false }, - ) - const ignored = yield* plugin.trigger( - "model.update", - {}, - { model: model("openai", "openai/gpt-5-chat"), cancel: false }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const openrouter = provider("openrouter", { + endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" }, + }) + catalog.provider.update(openrouter.id, (item) => { + item.endpoint = openrouter.endpoint + }) + catalog.provider.update(ProviderV2.ID.openai, () => {}) + for (const item of [ + model("openrouter", "openai/gpt-5-chat"), + model("openrouter", "openai/gpt-5"), + model("openai", "openai/gpt-5-chat"), + ]) { + catalog.model.update(item.providerID, item.id, () => {}) + } + }) - expect(result.cancel).toBe(true) - expect(regular.cancel).toBe(false) - expect(ignored.cancel).toBe(false) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat"))).enabled, + ).toBe(false) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled, + ).toBe(true) + expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled).toBe(true) }), ) - it.effect("does not filter gpt-5-chat-latest for non-OpenRouter providers", () => + it.effect("does not disable gpt-5-chat-latest for non-OpenRouter providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(OpenRouterPlugin) - const result = yield* plugin.trigger( - "model.update", - {}, - { model: model("custom-openrouter", "gpt-5-chat-latest"), cancel: false }, - ) - expect(result.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {}) + catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + }) + expect( + (yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"))) + .enabled, + ).toBe(true) }), ) }) diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 53045ca47f3..73471777f9b 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -1,25 +1,29 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel" +import { ProviderV2 } from "@opencode-ai/core/provider" import { it, model, provider } from "./provider-helper" describe("VercelPlugin", () => { it.effect("applies legacy lower-case referer headers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(VercelPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("vercel", { - options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) - expect(result.provider.options.headers).toEqual({ + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("vercel", { + endpoint: { type: "aisdk", package: "@ai-sdk/vercel" }, + options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).toEqual({ Existing: "1", "http-referer": "https://kilo.ai/", "x-title": "Kilo Code", @@ -30,10 +34,19 @@ describe("VercelPlugin", () => { it.effect("does not add legacy upper-case referer headers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(VercelPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("vercel"), cancel: false }) - expect(result.provider.options.headers).not.toHaveProperty("HTTP-Referer") - expect(result.provider.options.headers).not.toHaveProperty("X-Title") + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("vercel", { endpoint: { type: "aisdk", package: "@ai-sdk/vercel" } }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + }) + }) + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty( + "HTTP-Referer", + ) + expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty("X-Title") }), ) @@ -54,9 +67,11 @@ describe("VercelPlugin", () => { it.effect("ignores non-Vercel providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(VercelPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("gateway"), cancel: false }) - expect(result.provider.options.headers).toEqual({}) + const transform = yield* catalog.transform() + yield* transform((catalog) => catalog.provider.update(provider("gateway").id, () => {})) + expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).options.headers).toEqual({}) }), ) }) diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index be90baee926..f947c2be4fc 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -1,8 +1,10 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux" +import { ProviderV2 } from "@opencode-ai/core/provider" import { expectPluginRegistered, it, provider } from "./provider-helper" describe("ZenmuxPlugin", () => { @@ -18,30 +20,41 @@ describe("ZenmuxPlugin", () => { it.effect("applies the exact legacy Zenmux headers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(ZenmuxPlugin) - const result = yield* plugin.trigger("provider.update", {}, { provider: provider("zenmux"), cancel: false }) - expect(result.provider.options.headers).toEqual({ "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code" }) - expect(Object.keys(result.provider.options.headers).sort()).toEqual(["HTTP-Referer", "X-Title"]) - expect(result.cancel).toBe(false) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("zenmux", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + }) + }) + const result = yield* catalog.provider.get(ProviderV2.ID.make("zenmux")) + expect(result.options.headers).toEqual({ "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code" }) + expect(Object.keys(result.options.headers).sort()).toEqual(["HTTP-Referer", "X-Title"]) }), ) it.effect("merges legacy Zenmux headers with existing headers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(ZenmuxPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("zenmux", { - options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("zenmux", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) - expect(result.provider.options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://kilo.ai/", "X-Title": "Kilo Code", @@ -52,23 +65,25 @@ describe("ZenmuxPlugin", () => { it.effect("lets configured Zenmux legacy headers override defaults", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(ZenmuxPlugin) - const result = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("zenmux", { - options: { - headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, - body: {}, - aisdk: { provider: {}, request: {} }, - }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("zenmux", { + endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" }, + options: { + headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, + body: {}, + aisdk: { provider: {}, request: {} }, + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.endpoint = item.endpoint + draft.options = item.options + }) + }) - expect(result.provider.options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) @@ -78,23 +93,23 @@ describe("ZenmuxPlugin", () => { it.effect("guards legacy Zenmux headers to the exact zenmux provider id", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service yield* plugin.add(ZenmuxPlugin) - const ignored = yield* plugin.trigger( - "provider.update", - {}, - { - provider: provider("openrouter", { - options: { - headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, - body: {}, - aisdk: { provider: {}, request: {} }, - }, - }), - cancel: false, - }, - ) + const transform = yield* catalog.transform() + yield* transform((catalog) => { + const item = provider("openrouter", { + options: { + headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }, + body: {}, + aisdk: { provider: {}, request: {} }, + }, + }) + catalog.provider.update(item.id, (draft) => { + draft.options = item.options + }) + }) - expect(ignored.provider.options.headers).toEqual({ + expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) diff --git a/packages/core/test/policy.test.ts b/packages/core/test/policy.test.ts new file mode 100644 index 00000000000..c331b5585f8 --- /dev/null +++ b/packages/core/test/policy.test.ts @@ -0,0 +1,83 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { Location } from "@opencode-ai/core/location" +import { Policy } from "@opencode-ai/core/policy" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { location } from "./fixture/location" +import { testEffect } from "./lib/effect" + +const it = testEffect( + Policy.defaultLayer.pipe( + Layer.provide( + Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), + ), + ), +) + +describe("Policy", () => { + it.effect("returns the caller's fallback when no statement matches", () => + Effect.gen(function* () { + const policy = yield* Policy.Service + + expect(yield* policy.evaluate("provider.use", "anthropic", "allow")).toBe("allow") + expect(yield* policy.evaluate("provider.use", "anthropic", "deny")).toBe("deny") + }), + ) + + it.effect("evaluates wildcard provider rules in written order", () => + Effect.gen(function* () { + const policy = yield* Policy.Service + yield* policy.load([ + new Policy.Info({ + effect: "deny", + action: "provider.*", + resource: "*", + }), + new Policy.Info({ + effect: "allow", + action: "provider.use", + resource: "anthropic", + }), + ]) + + expect(yield* policy.evaluate("provider.use", "anthropic", "allow")).toBe("allow") + expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny") + }), + ) + + it.effect("matches action and resource independently", () => + Effect.gen(function* () { + const policy = yield* Policy.Service + yield* policy.load([ + new Policy.Info({ + effect: "deny", + action: "provider.*", + resource: "company-*", + }), + ]) + + expect(yield* policy.evaluate("provider.use", "company-stable", "allow")).toBe("deny") + expect(yield* policy.evaluate("plugin.load", "company-stable", "allow")).toBe("allow") + }), + ) + + it.effect("uses the last matching loaded statement", () => + Effect.gen(function* () { + const policy = yield* Policy.Service + yield* policy.load([ + new Policy.Info({ + effect: "allow", + action: "provider.use", + resource: "openai", + }), + new Policy.Info({ + effect: "deny", + action: "provider.use", + resource: "openai", + }), + ]) + + expect(yield* policy.evaluate("provider.use", "openai", "allow")).toBe("deny") + }), + ) +}) diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts new file mode 100644 index 00000000000..9980f65e547 --- /dev/null +++ b/packages/core/test/project.test.ts @@ -0,0 +1,220 @@ +import { describe, expect } from "bun:test" +import { $ } from "bun" +import fs from "fs/promises" +import path from "path" +import { Effect } from "effect" +import { Project } from "@opencode-ai/core/project" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { Hash } from "@opencode-ai/core/util/hash" +import { tmpdir } from "./fixture/tmpdir" +import { testEffect } from "./lib/effect" + +const it = testEffect(Project.defaultLayer) + +function remoteID(remote: string) { + return Project.ID.make(Hash.fast(`git-remote:${remote}`)) +} + +function abs(value: string) { + return AbsolutePath.make(value) +} + +function real(value: string) { + return Effect.promise(() => fs.realpath(value)).pipe(Effect.map((value) => AbsolutePath.make(value))) +} + +async function initRepo(dir: string, opts?: { commit?: boolean; remote?: string }) { + await $`git init`.cwd(dir).quiet() + await $`git config core.fsmonitor false`.cwd(dir).quiet() + await $`git config commit.gpgsign false`.cwd(dir).quiet() + await $`git config user.email test@opencode.test`.cwd(dir).quiet() + await $`git config user.name Test`.cwd(dir).quiet() + if (opts?.commit) await $`git commit --allow-empty -m root`.cwd(dir).quiet() + if (opts?.remote) await $`git remote add origin ${opts.remote}`.cwd(dir).quiet() +} + +async function rootCommit(dir: string) { + return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim() +} + +describe("ProjectV2.resolve", () => { + it.live("returns global for non-git directory", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.id).toBe(Project.ID.make("global")) + expect(path.resolve(result.directory)).toBe(path.parse(tmp.path).root) + expect(result.previous).toBeUndefined() + expect(result.vcs).toBeUndefined() + }), + ) + + it.live("returns git global for repo with no commits and no remote", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path)) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.id).toBe(Project.ID.make("global")) + expect(result.directory).toBe(yield* real(tmp.path)) + expect(result.previous).toBeUndefined() + expect(result.vcs?.type).toBe("git") + }), + ) + + it.live("falls back to root commit when origin is missing", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.directory).toBe(yield* real(tmp.path)) + expect(result.previous).toBeUndefined() + expect(result.vcs?.type).toBe("git") + }), + ) + + it.live("prefers normalized origin over root commit", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" })) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.id).toBe(remoteID("github.com/Acme/App")) + expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.directory).toBe(yield* real(tmp.path)) + expect(result.vcs?.type).toBe("git") + }), + ) + + it.live("normalizes ssh and https remotes to the same id", () => + Effect.gen(function* () { + const ssh = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + const https = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" })) + yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" })) + const project = yield* Project.Service + + const a = yield* project.resolve(abs(ssh.path)) + const b = yield* project.resolve(abs(https.path)) + + expect(a.id).toBe(remoteID("github.com/owner/repo")) + expect(b.id).toBe(a.id) + }), + ) + + it.live("ignores file remotes and falls back to root commit", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` })) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + }), + ) + + it.live("returns previous cached id from common dir", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) + yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "kilo"), "old-id")) // kilocode_change + const project = yield* Project.Service + + const result = yield* project.resolve(abs(tmp.path)) + + expect(result.previous).toBe(Project.ID.make("old-id")) + expect(result.id).toBe(remoteID("github.com/owner/repo")) + }), + ) + + it.live("does not write the cache while resolving", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) + const project = yield* Project.Service + + yield* project.resolve(abs(tmp.path)) + + expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "kilo")).exists())).toBe(false) // kilocode_change + }), + ) + + it.live("resolves from nested directories to repo root", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true })) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b"))) + + expect(result.directory).toBe(yield* real(tmp.path)) + }), + ) + + it.live("linked worktree returns opened worktree directory and previous from common dir", () => + Effect.gen(function* () { + const tmp = yield* Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ) + const worktree = `${tmp.path}-worktree` + yield* Effect.addFinalizer(() => + Effect.promise(() => $`rm -rf ${worktree}`.quiet().nothrow()).pipe(Effect.ignore), + ) + yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) + yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "kilo"), "old-id")) // kilocode_change + yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet()) + const project = yield* Project.Service + + const result = yield* project.resolve(abs(worktree)) + + expect(result.directory).toBe(yield* real(worktree)) + expect(result.previous).toBe(Project.ID.make("old-id")) + expect(result.id).toBe(remoteID("github.com/owner/repo")) + expect(result.vcs?.type).toBe("git") + }), + ) +}) diff --git a/packages/core/test/util/effect-flock.test.ts b/packages/core/test/util/effect-flock.test.ts index 3473ae5b491..98ea317d160 100644 --- a/packages/core/test/util/effect-flock.test.ts +++ b/packages/core/test/util/effect-flock.test.ts @@ -368,7 +368,7 @@ describe("util.effect-flock", () => { const proc = spawnWorker({ key: "eflock:crash", dir, ready, holdMs: 120_000 }) try { - await waitForFile(ready, 5_000) + await waitForFile(ready, 20_000) // kilocode_change - hosted macOS can start this worker slowly after stress tests await stopWorker(proc) // kilocode_change - stopWorker now awaits close before returning // Backdate lock files so they're past STALE_MS (60s) @@ -387,6 +387,6 @@ describe("util.effect-flock", () => { await fs.rm(tmp, { recursive: true, force: true }) } }), - 30_000, + 60_000, // kilocode_change - match the wider worker readiness window ) }) diff --git a/packages/effect-drizzle-sqlite/AGENTS.md b/packages/effect-drizzle-sqlite/AGENTS.md new file mode 100644 index 00000000000..39010a11273 --- /dev/null +++ b/packages/effect-drizzle-sqlite/AGENTS.md @@ -0,0 +1,19 @@ +# Effect Drizzle SQLite + +This package vendors a Drizzle Effect SQLite adapter for this repo. + +- Keep this package generic: Drizzle + Effect + SQLite only. +- Do not add opencode-specific tables, paths, migrations, post-commit hooks, or domain storage APIs here. +- Runtime code should depend on generic `effect/unstable/sql/SqlClient`, not a specific SQLite driver. +- Concrete SQLite clients such as `@effect/sql-sqlite-bun` belong in tests or examples unless this package intentionally adds a driver-specific helper. +- Preserve Drizzle adapter naming and behavior where possible so this can be replaced by upstream `drizzle-orm/effect-sqlite` later. +- If touching copied Drizzle internals, compare with current `drizzle-orm@1.0.0-rc.2` declarations and runtime JS. +- If touching Effect APIs, verify against `/Users/kit/code/open-source/effect-smol`. + +Useful entry points: + +- `src/effect-sqlite/driver.ts`: creates the Effect-backed Drizzle database with `make` and `makeWithDefaults`. +- `src/effect-sqlite/session.ts`: adapts generic Effect `SqlClient` execution and transactions to Drizzle SQLite sessions. +- `src/sqlite-core/effect/*`: Effect-yieldable SQLite query builders. +- `src/internal/drizzle-utils.ts`: local typed shims for Drizzle runtime internals that RC2 does not expose in declarations. +- `examples/basic.ts`: minimal usage example with Bun SQLite. diff --git a/packages/effect-drizzle-sqlite/examples/basic.ts b/packages/effect-drizzle-sqlite/examples/basic.ts new file mode 100644 index 00000000000..675aabcb857 --- /dev/null +++ b/packages/effect-drizzle-sqlite/examples/basic.ts @@ -0,0 +1,92 @@ +import { SqliteClient } from "@effect/sql-sqlite-bun" +import { eq } from "drizzle-orm" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import { EffectDrizzleSqlite } from "../src" + +const users = sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), +}) + +type User = typeof users.$inferSelect + +const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() +type DatabaseShape = Effect.Success + +const sqliteLayer = SqliteClient.layer({ filename: ":memory:", disableWAL: true }) + +class Database extends Context.Service()("@opencode/example/Database") { + static layer = Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer)) +} + +class UserStoreError extends Schema.TaggedErrorClass()("UserStoreError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +const mapStoreError = (message: string) => (cause: unknown) => new UserStoreError({ message, cause }) + +interface UserStoreShape { + migrate(): Effect.Effect + create(name: string): Effect.Effect + rename(from: string, to: string): Effect.Effect + list(): Effect.Effect +} + +class UserStore extends Context.Service()("@opencode/example/UserStore") { + static layer = Layer.effect( + UserStore, + Effect.gen(function* () { + const db = yield* Database + + return UserStore.of({ + migrate: Effect.fn("UserStore.migrate")(function* () { + yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: `${import.meta.dirname}/migrations` }).pipe( + Effect.mapError((cause) => new UserStoreError({ message: "Failed to migrate users", cause })), + ) + }), + create: Effect.fn("UserStore.create")(function* (name: string) { + yield* db + .insert(users) + .values({ name }) + .pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to create user"))) + }), + rename: Effect.fn("UserStore.rename")(function* (from: string, to: string) { + yield* db + .transaction( + Effect.fnUntraced(function* (tx) { + yield* tx.insert(users).values({ name: from }) + yield* tx.update(users).set({ name: to }).where(eq(users.name, from)) + }), + { behavior: "immediate" }, + ) + .pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to rename user"))) + }), + list: Effect.fn("UserStore.list")(function* () { + return yield* db + .select() + .from(users) + .pipe(Effect.mapError(mapStoreError("Failed to list users"))) + }), + }) + }), + ).pipe(Layer.provide(Database.layer)) +} + +const program = Effect.gen(function* () { + const userStore = yield* UserStore + + yield* userStore.migrate() + yield* userStore.create("Ada") + yield* userStore.rename("Grace", "Grace Hopper") + + return yield* userStore.list() +}) + +const rows = await Effect.runPromise(program.pipe(Effect.provide(UserStore.layer))) + +console.log(rows) diff --git a/packages/effect-drizzle-sqlite/examples/migrations/20240101000000_create_users/migration.sql b/packages/effect-drizzle-sqlite/examples/migrations/20240101000000_create_users/migration.sql new file mode 100644 index 00000000000..f1aa81cb454 --- /dev/null +++ b/packages/effect-drizzle-sqlite/examples/migrations/20240101000000_create_users/migration.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + name text NOT NULL +); diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json new file mode 100644 index 00000000000..c6b65536b58 --- /dev/null +++ b/packages/effect-drizzle-sqlite/package.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "7.3.54", + "name": "@opencode-ai/effect-drizzle-sqlite", + "type": "module", + "license": "MIT", + "private": true, + "scripts": { + "test": "bun test --timeout 30000", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", + "typecheck": "tsgo --noEmit" + }, + "exports": { + ".": "./src/index.ts", + "./effect-sqlite": "./src/effect-sqlite/index.ts", + "./effect-sqlite/migrator": "./src/effect-sqlite/migrator.ts", + "./sqlite-core/effect": "./src/sqlite-core/effect/index.ts" + }, + "devDependencies": { + "@effect/sql-sqlite-bun": "catalog:", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + }, + "dependencies": { + "drizzle-orm": "catalog:", + "effect": "catalog:" + }, + "peerDependencies": {} +} diff --git a/packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts b/packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts new file mode 100644 index 00000000000..f2ffc207d35 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts @@ -0,0 +1,77 @@ +/* oxlint-disable */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { SqlClient } from "effect/unstable/sql/SqlClient" +import { EffectCache } from "drizzle-orm/cache/core/cache-effect" +import { EffectLogger } from "drizzle-orm/effect-core" +import { entityKind } from "drizzle-orm/entity" +import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations" +import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" +import { SQLiteEffectDatabase } from "../sqlite-core/effect/db" +import type { DrizzleConfig } from "drizzle-orm/utils" +import { jitCompatCheck } from "../internal/drizzle-utils" +import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session" + +export class EffectSQLiteDatabase extends SQLiteEffectDatabase< + EffectSQLiteQueryEffectHKT, + EffectSQLiteRunResult, + TRelations +> { + static override readonly [entityKind]: string = "EffectSQLiteDatabase" +} + +export type EffectDrizzleSQLiteConfig = Omit< + DrizzleConfig, TRelations>, + "cache" | "logger" | "schema" +> + +export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default) + +/** + * Creates an EffectSQLiteDatabase instance. + * + * Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided. + * Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as + * `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`. + * + * @example + * ```ts + * import { SqliteClient } from '@effect/sql-sqlite-node'; + * import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite'; + * import * as Effect from 'effect/Effect'; + * + * const db = yield* SQLiteDrizzle.make({ relations }).pipe( + * Effect.provide(SQLiteDrizzle.DefaultServices), + * Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })), + * ); + * ``` + */ +export const make = Effect.fn("SQLiteDrizzle.make")(function* ( + config: EffectDrizzleSQLiteConfig = {}, +) { + const client = yield* SqlClient + const cache = yield* EffectCache + const logger = yield* EffectLogger + + const dialect = new SQLiteAsyncDialect() + const relations = config.relations ?? ({} as TRelations) + const session = new EffectSQLiteSession(client, dialect, relations, { + logger, + cache, + useJitMappers: jitCompatCheck(config.jit), + }) + const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase & { + $client: SqlClient + } + db.$client = client + db.$cache.invalidate = cache.onMutate + + return db +}) + +/** + * Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided. + */ +export const makeWithDefaults = ( + config: EffectDrizzleSQLiteConfig = {}, +) => make(config).pipe(Effect.provide(DefaultServices)) diff --git a/packages/effect-drizzle-sqlite/src/effect-sqlite/index.ts b/packages/effect-drizzle-sqlite/src/effect-sqlite/index.ts new file mode 100644 index 00000000000..1133e48b3a5 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/effect-sqlite/index.ts @@ -0,0 +1,4 @@ +/* oxlint-disable */ +export { EffectLogger } from "drizzle-orm/effect-core" +export * from "./driver" +export * from "./session" diff --git a/packages/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts b/packages/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts new file mode 100644 index 00000000000..6d0d1551437 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts @@ -0,0 +1,14 @@ +/* oxlint-disable */ +import type { MigrationConfig } from "drizzle-orm/migrator" +import { readMigrationFiles } from "drizzle-orm/migrator" +import type { AnyRelations } from "drizzle-orm/relations" +import { migrate as coreMigrate } from "../sqlite-core/effect/session" +import type { EffectSQLiteDatabase } from "./driver" + +export function migrate( + db: EffectSQLiteDatabase, + config: MigrationConfig, +) { + const migrations = readMigrationFiles(config) + return coreMigrate(migrations, db.session, config) +} diff --git a/packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts b/packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts new file mode 100644 index 00000000000..047b50b61f4 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts @@ -0,0 +1,214 @@ +/* oxlint-disable */ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Scope from "effect/Scope" +import type { SqlClient } from "effect/unstable/sql/SqlClient" +import type { SqlError } from "effect/unstable/sql/SqlError" +import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" +import type { WithCacheConfig } from "drizzle-orm/cache/core/types" +import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors" +import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger" +import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import type { AnyRelations } from "drizzle-orm/relations" +import type { RelationalQueryMapperConfig } from "drizzle-orm/relations" +import type { Query } from "drizzle-orm/sql/sql" +import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" +import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session" +import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" + +export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase { + readonly error: EffectDrizzleQueryError + readonly context: never +} + +export type EffectSQLiteRunResult = readonly never[] + +export interface EffectSQLiteSessionOptions { + logger: EffectLoggerShape + cache: EffectCacheShape + useJitMappers?: boolean +} + +export class EffectSQLiteSession extends SQLiteEffectSession< + EffectSQLiteQueryEffectHKT, + EffectSQLiteRunResult, + TRelations +> { + static override readonly [entityKind]: string = "EffectSQLiteSession" + + constructor( + private client: SqlClient, + dialect: SQLiteAsyncDialect, + protected relations: TRelations, + private options: EffectSQLiteSessionOptions, + ) { + super(dialect) + } + + override prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + queryMetadata?: { + type: "select" | "update" | "delete" | "insert" + tables: string[] + }, + cacheConfig?: WithCacheConfig, + ): SQLiteEffectPreparedQuery { + return new SQLiteEffectPreparedQuery( + (params, method) => this.execute(query, params, method), + query, + this.options.logger, + this.options.cache, + queryMetadata, + cacheConfig, + fields, + executeMethod, + this.options.useJitMappers, + customResultMapper, + undefined, + undefined, + this.isInTransaction(), + ) + } + + override prepareRelationalQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper: (rows: Record[], mapColumnValue?: (value: unknown) => unknown) => unknown, + config: RelationalQueryMapperConfig, + ): SQLiteEffectPreparedQuery { + return new SQLiteEffectPreparedQuery( + (params, method) => this.execute(query, params, method), + query, + this.options.logger, + this.options.cache, + undefined, + undefined, + fields, + executeMethod, + this.options.useJitMappers, + customResultMapper, + true, + config, + this.isInTransaction(), + ) + } + + private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") { + const statement = this.client.unsafe(query.sql, params) + if (method === "values") return statement.values + if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0])) + return statement.withoutTransform + } + + private isInTransaction() { + return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some")) + } + + private executeTransactionStatement(connection: Effect.Success, query: string) { + return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid) + } + + private withTransaction(effect: Effect.Effect, config: SQLiteTransactionConfig | undefined) { + return Effect.uninterruptibleMask((restore) => + Effect.withFiber((fiber) => { + const services = fiber.context + const connectionOption = Context.getOption(services, this.client.transactionService) + const connection: Effect.Effect< + readonly [Scope.Closeable | undefined, Effect.Success], + SqlError + > = + connectionOption._tag === "Some" + ? Effect.succeed([undefined, connectionOption.value[0]] as const) + : Scope.make().pipe( + Effect.flatMap((scope) => + Scope.provide(this.client.reserve, scope).pipe( + Effect.map((connection) => [scope, connection] as const), + Effect.catch((error) => + Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))), + ), + ), + ), + ) + const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0 + + return connection.pipe( + Effect.flatMap(([scope, connection]) => + this.executeTransactionStatement( + connection, + id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`, + ).pipe( + Effect.flatMap(() => + Effect.provideContext( + restore(effect), + Context.add(services, this.client.transactionService, [connection, id]), + ), + ), + Effect.exit, + Effect.flatMap((exit) => { + const finalize = Exit.isSuccess(exit) + ? id === 0 + ? this.executeTransactionStatement(connection, "commit").pipe( + // SQLite keeps the transaction open after deferred constraint commit failures. + Effect.catch((error) => + this.executeTransactionStatement(connection, "rollback").pipe( + Effect.catch(() => Effect.void), + Effect.andThen(Effect.fail(error)), + ), + ), + ) + : this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`) + : id === 0 + ? this.executeTransactionStatement(connection, "rollback") + : this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe( + Effect.andThen( + this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`), + ), + ) + const scoped = scope === undefined ? finalize : Effect.ensuring(finalize, Scope.close(scope, exit)) + + return scoped.pipe(Effect.flatMap(() => exit)) + }), + ), + ), + ) + }), + ) + } + + override transaction( + transaction: (tx: EffectSQLiteTransaction) => Effect.Effect, + config?: SQLiteTransactionConfig, + ): Effect.Effect { + const { dialect, relations } = this + + return this.withTransaction( + Effect.gen({ self: this }, function* () { + const tx = new EffectSQLiteTransaction(dialect, this, relations) + + return yield* transaction(tx) + }), + config, + ) + } +} + +export class EffectSQLiteTransaction extends SQLiteEffectTransaction< + EffectSQLiteQueryEffectHKT, + EffectSQLiteRunResult, + TRelations +> { + static override readonly [entityKind]: string = "EffectSQLiteTransaction" + + override transaction: ( + transaction: ( + tx: SQLiteEffectTransaction, + ) => Effect.Effect, + ) => Effect.Effect = (tx) => this.session.transaction(tx) +} diff --git a/packages/effect-drizzle-sqlite/src/index.ts b/packages/effect-drizzle-sqlite/src/index.ts new file mode 100644 index 00000000000..d6606b7d9fe --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/index.ts @@ -0,0 +1,6 @@ +export { EffectLogger } from "drizzle-orm/effect-core" +export * from "./effect-sqlite/driver" +export * from "./effect-sqlite/session" +export { migrate } from "./effect-sqlite/migrator" + +export * as EffectDrizzleSqlite from "." diff --git a/packages/effect-drizzle-sqlite/src/internal/drizzle-utils.ts b/packages/effect-drizzle-sqlite/src/internal/drizzle-utils.ts new file mode 100644 index 00000000000..1998a08318c --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/internal/drizzle-utils.ts @@ -0,0 +1,127 @@ +/* oxlint-disable */ +import { Column, getColumnTable } from "drizzle-orm/column" +import { is } from "drizzle-orm/entity" +import type { JoinNullability } from "drizzle-orm/query-builders/select.types" +import { Param, SQL } from "drizzle-orm/sql/sql" +import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" +import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" +import { Subquery } from "drizzle-orm/subquery" +import { Table, getTableName } from "drizzle-orm/table" +import type { UpdateSet } from "drizzle-orm/utils" +import { ViewBaseConfig } from "drizzle-orm/view-common" + +const TableSymbol = ( + Table as unknown as { + Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol } + } +).Symbol + +export function getTableColumnsRuntime(table: SQLiteTable) { + return (table as unknown as Record>)[TableSymbol.Columns] +} + +export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) { + return (view as unknown as Record; name: string }>)[ViewBaseConfig] +} + +export function jitCompatCheck(isEnabled: boolean | undefined) { + if (!isEnabled) return false + try { + return new Function("input", '"use strict"; return input;')(true) === true + } catch { + return false + } +} + +export function orderSelectedFields( + fields: Record, + pathPrefix?: string[], +): SelectedFieldsOrdered { + return Object.entries(fields).flatMap(([name, field]) => { + const path = pathPrefix ? [...pathPrefix, name] : [name] + if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) { + return [{ path, field }] as SelectedFieldsOrdered + } + if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path) + return orderSelectedFields(field as Record, path) + }) as SelectedFieldsOrdered +} + +export function mapUpdateSet(table: TTable, values: SQLiteUpdateSetSource) { + const entries = Object.entries(values).filter(([, value]) => value !== undefined) + if (entries.length === 0) throw new Error("No values to set") + + return Object.fromEntries( + entries.map(([key, value]) => [ + key, + is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]), + ]), + ) as UpdateSet +} + +export function mapResultRow( + columns: SelectedFieldsOrdered, + row: unknown[], + joinsNotNullableMap: Record | undefined, +) { + const nullifyMap: Record = {} + const result: Record = {} + + columns.forEach((column, columnIndex) => { + const decoder = ( + is(column.field, Column) + ? column.field + : is(column.field, SQL) + ? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + : is(column.field, Subquery) + ? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + : (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder + ) as { + mapFromDriverValue(value: unknown): unknown + } + const rawValue = row[columnIndex] + const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue) + const objectName = column.path[0] + let node = result + + column.path.forEach((pathChunk, pathChunkIndex) => { + if (pathChunkIndex === column.path.length - 1) { + node[pathChunk] = value + return + } + node[pathChunk] = (node[pathChunk] ?? {}) as Record + node = node[pathChunk] as Record + }) + + if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) { + const tableName = getTableName(getColumnTable(column.field)) + nullifyMap[objectName] = + !(objectName in nullifyMap) && value === null + ? tableName + : typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName + ? false + : nullifyMap[objectName] + } + }) + + Object.entries(nullifyMap).forEach(([objectName, tableName]) => { + if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null + }) + + return result +} + +export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) { + if (is(table, Subquery)) return table._.alias + if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name + if (is(table, SQL)) return undefined + return (table as unknown as Record)[ + (table as unknown as Record)[TableSymbol.IsAlias] + ? TableSymbol.Name + : TableSymbol.BaseName + ] as string +} + +export type { JoinNullability } diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts new file mode 100644 index 00000000000..c420d6fbb05 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts @@ -0,0 +1,58 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import type { SQLiteView } from "drizzle-orm/sqlite-core/view" +import type { SQLiteEffectSession } from "./session" + +function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL) { + return sql`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})` +} + +function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL) { + return sql`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}` +} + +export interface SQLiteEffectCountBuilder + extends SQL, + SQLWrapper, + Effect.Effect {} + +export class SQLiteEffectCountBuilder extends SQL { + static override readonly [entityKind]: string = "SQLiteEffectCountBuilder" + + private sql: SQL + private session: SQLiteEffectSession + + constructor(params: { + source: SQLiteTable | SQLiteView | SQL | SQLWrapper + filters?: SQL + session: SQLiteEffectSession + }) { + super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks) + + this.session = params.session + this.sql = buildSQLiteCount(params.source, params.filters) + } + + execute(placeholderValues?: Record) { + return this.session + .prepareQuery<{ + type: "async" + execute: number + run: unknown + all: unknown + get: unknown + values: unknown + }>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => { + const v = rows[0]?.[0] + if (typeof v === "number") return v + return v ? Number(v) : 0 + }) + .execute(placeholderValues) + } +} + +applyEffectWrapper(SQLiteEffectCountBuilder) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts new file mode 100644 index 00000000000..ac4cc140ad0 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts @@ -0,0 +1,296 @@ +/* oxlint-disable */ +import { Effect } from "effect" +import type { SqlError } from "effect/unstable/sql/SqlError" +import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" +import type { MutationOption } from "drizzle-orm/cache/core/cache" +import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import type { TypedQueryBuilder } from "drizzle-orm/query-builders/query-builder" +import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations" +import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" +import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" +import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder" +import type { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" +import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import type { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" +import { WithSubquery } from "drizzle-orm/subquery" +import type { WithBuilder } from "drizzle-orm/sqlite-core/subquery" +import { SQLiteEffectCountBuilder } from "./count" +import { SQLiteEffectDeleteBase } from "./delete" +import { SQLiteEffectInsertBuilder } from "./insert" +import { SQLiteEffectRelationalQueryBuilder } from "./query" +import { SQLiteEffectRaw } from "./raw" +import { SQLiteEffectSelectBuilder } from "./select" +import type { SQLiteEffectSelectBase } from "./select" +import type { SQLiteEffectSession, SQLiteEffectTransaction } from "./session" +import { SQLiteEffectUpdateBuilder } from "./update" + +export class SQLiteEffectDatabase< + TEffectHKT extends QueryEffectHKTBase, + TRunResult, + TRelations extends AnyRelations = EmptyRelations, +> { + static readonly [entityKind]: string = "SQLiteEffectDatabase" + + declare readonly _: { + readonly relations: TRelations + readonly session: SQLiteEffectSession + } + + query: { + [K in keyof TRelations]: SQLiteEffectRelationalQueryBuilder + } + + constructor( + /** @internal */ + readonly dialect: SQLiteAsyncDialect, + /** @internal */ + readonly session: SQLiteEffectSession, + relations: TRelations, + readonly rowModeRQB?: boolean, + readonly forbidJsonb?: boolean, + ) { + this._ = { + relations, + session, + } + + this.query = {} as (typeof this)["query"] + for (const [tableName, relation] of Object.entries(relations)) { + ;(this.query as SQLiteEffectDatabase["query"])[tableName] = + new SQLiteEffectRelationalQueryBuilder( + relations, + relations[relation.name]!.table as SQLiteTable, + relation, + dialect, + session, + rowModeRQB, + forbidJsonb, + ) + } + + this.$cache = { + invalidate: (_params: MutationOption) => Effect.void, + } + } + + $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => { + const self = this + const as = ( + qb: + | TypedQueryBuilder + | SQL + | ((qb: QueryBuilder) => TypedQueryBuilder | SQL), + ) => { + if (typeof qb === "function") { + qb = qb(new QueryBuilder(self.dialect)) + } + + return new Proxy( + new WithSubquery( + qb.getSQL(), + selection ?? + (("getSelectedFields" in qb + ? ((qb as { getSelectedFields(): SelectedFields | undefined }).getSelectedFields() ?? {}) + : {}) as SelectedFields), + alias, + true, + ), + new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }), + ) + } + return { as } + } + + $cache: { invalidate: EffectCacheShape["onMutate"] } + + $count(source: SQLiteTable | SQLiteViewBase | SQL | SQLWrapper, filters?: SQL) { + return new SQLiteEffectCountBuilder({ source, filters, session: this.session }) + } + + with(...queries: WithSubquery[]) { + const self = this + + function select(): SQLiteEffectSelectBuilder + function select( + fields: TSelection, + ): SQLiteEffectSelectBuilder + function select( + fields?: SelectedFields, + ): SQLiteEffectSelectBuilder { + return new SQLiteEffectSelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + }) + } + + function selectDistinct(): SQLiteEffectSelectBuilder + function selectDistinct( + fields: TSelection, + ): SQLiteEffectSelectBuilder + function selectDistinct( + fields?: SelectedFields, + ): SQLiteEffectSelectBuilder { + return new SQLiteEffectSelectBuilder({ + fields: fields ?? undefined, + session: self.session, + dialect: self.dialect, + withList: queries, + distinct: true, + }) + } + + function update( + table: TTable, + ): SQLiteEffectUpdateBuilder { + return new SQLiteEffectUpdateBuilder(table, self.session, self.dialect, queries) + } + + function insert( + into: TTable, + ): SQLiteEffectInsertBuilder { + return new SQLiteEffectInsertBuilder(into, self.session, self.dialect, queries) + } + + function delete_( + from: TTable, + ): SQLiteEffectDeleteBase { + return new SQLiteEffectDeleteBase(from, self.session, self.dialect, queries) + } + + return { select, selectDistinct, update, insert, delete: delete_ } + } + + select(): SQLiteEffectSelectBuilder + select( + fields: TSelection, + ): SQLiteEffectSelectBuilder + select(fields?: SelectedFields): SQLiteEffectSelectBuilder { + return new SQLiteEffectSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect }) + } + + selectDistinct(): SQLiteEffectSelectBuilder + selectDistinct( + fields: TSelection, + ): SQLiteEffectSelectBuilder + selectDistinct( + fields?: SelectedFields, + ): SQLiteEffectSelectBuilder { + return new SQLiteEffectSelectBuilder({ + fields: fields ?? undefined, + session: this.session, + dialect: this.dialect, + distinct: true, + }) + } + + update(table: TTable): SQLiteEffectUpdateBuilder { + return new SQLiteEffectUpdateBuilder(table, this.session, this.dialect) + } + + insert(into: TTable): SQLiteEffectInsertBuilder { + return new SQLiteEffectInsertBuilder(into, this.session, this.dialect) + } + + delete( + from: TTable, + ): SQLiteEffectDeleteBase { + return new SQLiteEffectDeleteBase(from, this.session, this.dialect) + } + + private raw( + query: SQLWrapper | string, + action: "all" | "get" | "run" | "values", + execute: (query: SQL) => Effect.Effect, + ): SQLiteEffectRaw { + const sequel = typeof query === "string" ? sql.raw(query) : query.getSQL() + return new SQLiteEffectRaw( + () => execute(sequel), + () => sequel, + action, + this.dialect, + (result) => result, + ) + } + + run(query: SQLWrapper | string): SQLiteEffectRaw { + return this.raw(query, "run", (sequel) => this.session.run(sequel)) + } + + all(query: SQLWrapper | string): SQLiteEffectRaw { + return this.raw(query, "all", (sequel) => this.session.all(sequel)) + } + + get(query: SQLWrapper | string): SQLiteEffectRaw { + return this.raw(query, "get", (sequel) => this.session.get(sequel)) + } + + values(query: SQLWrapper | string): SQLiteEffectRaw { + return this.raw(query, "values", (sequel) => this.session.values(sequel)) + } + + transaction: ( + transaction: (tx: SQLiteEffectTransaction) => Effect.Effect, + config?: SQLiteTransactionConfig, + ) => Effect.Effect = (tx, config) => this.session.transaction(tx, config) +} + +export type SQLiteEffectWithReplicas = Q & { $primary: Q; $replicas: Q[] } + +export const withReplicas = < + TEffectHKT extends QueryEffectHKTBase, + TRunResult, + TRelations extends AnyRelations, + Q extends SQLiteEffectDatabase, +>( + primary: Q, + replicas: [Q, ...Q[]], + getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!, +): SQLiteEffectWithReplicas => { + const select: Q["select"] = (...args: []) => getReplica(replicas).select(...args) + const selectDistinct: Q["selectDistinct"] = (...args: []) => getReplica(replicas).selectDistinct(...args) + const $count: Q["$count"] = (...args: [any]) => getReplica(replicas).$count(...args) + const _with: Q["with"] = (...args: []) => getReplica(replicas).with(...args) + const $with = ((...args: [string] | [string, ColumnsSelection]) => + args.length === 1 + ? getReplica(replicas).$with(args[0]) + : getReplica(replicas).$with(args[0], args[1])) as Q["$with"] + + const update: Q["update"] = (...args: [any]) => primary.update(...args) + const insert: Q["insert"] = (...args: [any]) => primary.insert(...args) + const $delete: Q["delete"] = (...args: [any]) => primary.delete(...args) + const run: Q["run"] = (...args: [any]) => primary.run(...args) + const all: Q["all"] = (...args: [any]) => primary.all(...args) + const get: Q["get"] = (...args: [any]) => primary.get(...args) + const values: Q["values"] = (...args: [any]) => primary.values(...args) + const transaction: Q["transaction"] = (...args: [any]) => primary.transaction(...args) + + return { + ...primary, + update, + insert, + delete: $delete, + run, + all, + get, + values, + transaction, + $primary: primary, + $replicas: replicas, + select, + selectDistinct, + $count, + $with, + with: _with, + get query() { + return getReplica(replicas).query + }, + } +} + +export type AnySQLiteEffectDatabase = SQLiteEffectDatabase +export type AnySQLiteEffectSelectBase = SQLiteEffectSelectBase diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts new file mode 100644 index 00000000000..15e90d19746 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts @@ -0,0 +1,261 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" +import type { RunnableQuery } from "drizzle-orm/runnable-query" +import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" +import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" +import type { SQLiteDeleteConfig } from "drizzle-orm/sqlite-core/query-builders/delete" +import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" +import { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" +import type { Subquery } from "drizzle-orm/subquery" +import { type DrizzleTypeError, type ValueOrArray } from "drizzle-orm/utils" +import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" +import { getTableColumnsRuntime, orderSelectedFields } from "../../internal/drizzle-utils" +import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + +export type SQLiteEffectDeleteWithout< + T extends AnySQLiteEffectDelete, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true + ? T + : Omit< + SQLiteEffectDeleteBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["returning"], + TDynamic, + T["_"]["excludedMethods"] | K, + T["_"]["effectHKT"] + >, + T["_"]["excludedMethods"] | K + > + +export type SQLiteEffectDeleteReturningAll< + T extends AnySQLiteEffectDelete, + TDynamic extends boolean, +> = SQLiteEffectDeleteWithout< + SQLiteEffectDeleteBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["table"]["$inferSelect"], + T["_"]["dynamic"], + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectDeleteReturning< + T extends AnySQLiteEffectDelete, + TDynamic extends boolean, + TSelectedFields extends SelectedFieldsFlat, +> = SQLiteEffectDeleteWithout< + SQLiteEffectDeleteBase< + T["_"]["table"], + T["_"]["runResult"], + SelectResultFields, + T["_"]["dynamic"], + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectDeleteExecute = T["_"]["returning"] extends undefined + ? T["_"]["runResult"] + : T["_"]["returning"][] + +export type SQLiteEffectDeletePrepare< + T extends AnySQLiteEffectDelete, + TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], +> = SQLiteEffectPreparedQuery< + PreparedQueryConfig & { + run: T["_"]["runResult"] + all: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".all() cannot be used without .returning()"> + : T["_"]["returning"][] + get: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".get() cannot be used without .returning()"> + : T["_"]["returning"] | undefined + values: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".values() cannot be used without .returning()"> + : any[][] + execute: SQLiteEffectDeleteExecute + }, + TEffectHKT +> + +export type SQLiteEffectDeleteDynamic = SQLiteEffectDelete< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["returning"], + T["_"]["effectHKT"] +> + +export type SQLiteEffectDelete< + TTable extends SQLiteTable = SQLiteTable, + TRunResult = unknown, + TReturning extends Record | undefined = undefined, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> = SQLiteEffectDeleteBase + +export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase + +export interface SQLiteEffectDeleteBase< + TTable extends SQLiteTable, + TRunResult, + TReturning extends Record | undefined = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> extends RunnableQuery, + SQLWrapper, + Effect.Effect< + TReturning extends undefined ? TRunResult : TReturning[], + TEffectHKT["error"], + TEffectHKT["context"] + > { + readonly _: { + dialect: "sqlite" + readonly table: TTable + readonly resultType: "async" + readonly runResult: TRunResult + readonly returning: TReturning + readonly dynamic: TDynamic + readonly excludedMethods: _TExcludedMethods + readonly result: TReturning extends undefined ? TRunResult : TReturning[] + readonly effectHKT: TEffectHKT + } +} + +export class SQLiteEffectDeleteBase< + TTable extends SQLiteTable, + TRunResult, + TReturning extends Record | undefined = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + > + implements RunnableQuery, SQLWrapper +{ + static readonly [entityKind]: string = "SQLiteEffectDelete" + + /** @internal */ + config: SQLiteDeleteConfig + + constructor( + private table: TTable, + private effectSession: SQLiteEffectSession, + private effectDialect: SQLiteDialect, + withList?: Subquery[], + ) { + this.config = { table, withList } + } + + where(where: SQL | undefined): SQLiteEffectDeleteWithout { + this.config.where = where + return this as any + } + + orderBy( + builder: (deleteTable: TTable) => ValueOrArray, + ): SQLiteEffectDeleteWithout + orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectDeleteWithout + orderBy( + ...columns: + | [(deleteTable: TTable) => ValueOrArray] + | (SQLiteColumn | SQL | SQL.Aliased)[] + ): SQLiteEffectDeleteWithout { + if (typeof columns[0] === "function") { + const orderBy = columns[0]( + new Proxy( + getTableColumnsRuntime(this.config.table), + new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }), + ) as any, + ) + + this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy] + return this as any + } + + this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[] + return this as any + } + + limit(limit: number | Placeholder): SQLiteEffectDeleteWithout { + this.config.limit = limit + return this as any + } + + returning(): SQLiteEffectDeleteReturningAll + returning( + fields: TSelectedFields, + ): SQLiteEffectDeleteReturning + returning( + fields: SelectedFieldsFlat = getTableColumnsRuntime(this.table), + ): SQLiteEffectDeleteReturning | SQLiteEffectDeleteReturningAll { + this.config.returning = orderSelectedFields(fields) + return this as any + } + + /** @internal */ + getSQL(): SQL { + return this.effectDialect.buildDeleteQuery(this.config) + } + + toSQL(): Query { + return this.effectDialect.sqlToQuery(this.getSQL()) + } + + /** @internal */ + _prepare(isOneTimeQuery = true): SQLiteEffectDeletePrepare { + return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + this.effectDialect.sqlToQuery(this.getSQL()), + this.config.returning, + this.config.returning ? "all" : "run", + undefined, + { + type: "delete", + tables: extractUsedTable(this.config.table), + }, + ) as SQLiteEffectDeletePrepare + } + + prepare(): SQLiteEffectDeletePrepare { + return this._prepare(false) + } + + run: ReturnType["run"] = (placeholderValues) => { + return this._prepare().run(placeholderValues) + } + + all: ReturnType["all"] = (placeholderValues) => { + return this._prepare().all(placeholderValues) + } + + get: ReturnType["get"] = (placeholderValues) => { + return this._prepare().get(placeholderValues) + } + + values: ReturnType["values"] = (placeholderValues) => { + return this._prepare().values(placeholderValues) + } + + execute: ReturnType["execute"] = (placeholderValues) => { + return this._prepare().execute(placeholderValues) + } + + $dynamic(): SQLiteEffectDeleteDynamic { + return this as any + } +} + +applyEffectWrapper(SQLiteEffectDeleteBase) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/index.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/index.ts new file mode 100644 index 00000000000..ab7aeb1ab60 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/index.ts @@ -0,0 +1,10 @@ +/* oxlint-disable */ +export * from "./count" +export * from "./db" +export * from "./delete" +export * from "./insert" +export * from "./query" +export * from "./raw" +export * from "./select" +export * from "./session" +export * from "./update" diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts new file mode 100644 index 00000000000..f85dc244756 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts @@ -0,0 +1,349 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind, is } from "drizzle-orm/entity" +import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" +import type { RunnableQuery } from "drizzle-orm/runnable-query" +import type { Query, SQLWrapper } from "drizzle-orm/sql/sql" +import { Param, SQL, sql } from "drizzle-orm/sql/sql" +import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" +import type { IndexColumn } from "drizzle-orm/sqlite-core/indexes" +import type { + SQLiteInsertConfig, + SQLiteInsertSelectQueryBuilder, + SQLiteInsertValue, +} from "drizzle-orm/sqlite-core/query-builders/insert" +import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" +import { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" +import type { Subquery } from "drizzle-orm/subquery" +import { type DrizzleTypeError, haveSameKeys } from "drizzle-orm/utils" +import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" +import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder" +import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" +import { getTableColumnsRuntime, mapUpdateSet, orderSelectedFields } from "../../internal/drizzle-utils" +import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + +export type SQLiteEffectInsertWithout< + T extends AnySQLiteEffectInsert, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true + ? T + : Omit< + SQLiteEffectInsertBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["returning"], + TDynamic, + T["_"]["excludedMethods"] | K, + T["_"]["effectHKT"] + >, + T["_"]["excludedMethods"] | K + > + +export type SQLiteEffectInsertReturning< + T extends AnySQLiteEffectInsert, + TDynamic extends boolean, + TSelectedFields extends SelectedFieldsFlat, +> = SQLiteEffectInsertWithout< + SQLiteEffectInsertBase< + T["_"]["table"], + T["_"]["runResult"], + SelectResultFields, + TDynamic, + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectInsertReturningAll< + T extends AnySQLiteEffectInsert, + TDynamic extends boolean, +> = SQLiteEffectInsertWithout< + SQLiteEffectInsertBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["table"]["$inferSelect"], + TDynamic, + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectInsertDynamic = SQLiteEffectInsert< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["returning"], + T["_"]["effectHKT"] +> + +export type SQLiteEffectInsertOnConflictDoUpdateConfig = { + target: IndexColumn | IndexColumn[] + /** @deprecated - use either `targetWhere` or `setWhere` */ + where?: SQL + targetWhere?: SQL + setWhere?: SQL + set: SQLiteUpdateSetSource +} + +export type SQLiteEffectInsertExecute = T["_"]["returning"] extends undefined + ? T["_"]["runResult"] + : T["_"]["returning"][] + +export type SQLiteEffectInsertPrepare< + T extends AnySQLiteEffectInsert, + TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], +> = SQLiteEffectPreparedQuery< + PreparedQueryConfig & { + run: T["_"]["runResult"] + all: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".all() cannot be used without .returning()"> + : T["_"]["returning"][] + get: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".get() cannot be used without .returning()"> + : T["_"]["returning"] + values: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".values() cannot be used without .returning()"> + : any[][] + execute: SQLiteEffectInsertExecute + }, + TEffectHKT +> + +export type SQLiteEffectInsert< + TTable extends SQLiteTable = SQLiteTable, + TRunResult = unknown, + TReturning = any, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> = SQLiteEffectInsertBase + +export type AnySQLiteEffectInsert = SQLiteEffectInsertBase + +export class SQLiteEffectInsertBuilder< + TTable extends SQLiteTable, + TRunResult, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> { + static readonly [entityKind]: string = "SQLiteEffectInsertBuilder" + + constructor( + protected table: TTable, + protected session: SQLiteEffectSession, + protected dialect: SQLiteDialect, + private withList?: Subquery[], + ) {} + + values( + value: SQLiteInsertValue, + ): SQLiteEffectInsertBase + values( + values: SQLiteInsertValue[], + ): SQLiteEffectInsertBase + values( + values: SQLiteInsertValue | SQLiteInsertValue[], + ): SQLiteEffectInsertBase { + values = Array.isArray(values) ? values : [values] + if (values.length === 0) { + throw new Error("values() must be called with at least one value") + } + const mappedValues = values.map((entry) => { + const result: Record = {} + const cols = getTableColumnsRuntime(this.table) + for (const colKey of Object.keys(entry)) { + const colValue = entry[colKey as keyof typeof entry] + result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey]) + } + return result + }) + + return new SQLiteEffectInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList) + } + + select( + selectQuery: (qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder, + ): SQLiteEffectInsertBase + select( + selectQuery: (qb: QueryBuilder) => SQL, + ): SQLiteEffectInsertBase + select(selectQuery: SQL): SQLiteEffectInsertBase + select( + selectQuery: SQLiteInsertSelectQueryBuilder, + ): SQLiteEffectInsertBase + select( + selectQuery: + | SQL + | SQLiteInsertSelectQueryBuilder + | ((qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder | SQL), + ): SQLiteEffectInsertBase { + const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery + + if (!is(select, SQL) && !haveSameKeys(getTableColumnsRuntime(this.table), select._.selectedFields)) { + throw new Error( + "Insert select error: selected fields are not the same or are in a different order compared to the table definition", + ) + } + + return new SQLiteEffectInsertBase(this.table, select, this.session, this.dialect, this.withList, true) + } +} + +export interface SQLiteEffectInsertBase< + TTable extends SQLiteTable, + TRunResult, + TReturning = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> extends SQLWrapper, + RunnableQuery, + Effect.Effect< + TReturning extends undefined ? TRunResult : TReturning[], + TEffectHKT["error"], + TEffectHKT["context"] + > { + readonly _: { + readonly dialect: "sqlite" + readonly table: TTable + readonly resultType: "async" + readonly runResult: TRunResult + readonly returning: TReturning + readonly dynamic: TDynamic + readonly excludedMethods: _TExcludedMethods + readonly result: TReturning extends undefined ? TRunResult : TReturning[] + readonly effectHKT: TEffectHKT + } +} + +export class SQLiteEffectInsertBase< + TTable extends SQLiteTable, + TRunResult, + TReturning = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + > + implements RunnableQuery, SQLWrapper +{ + static readonly [entityKind]: string = "SQLiteEffectInsert" + + /** @internal */ + config: SQLiteInsertConfig + + constructor( + private table: TTable, + values: SQLiteInsertConfig["values"], + private effectSession: SQLiteEffectSession, + private effectDialect: SQLiteDialect, + withList?: Subquery[], + select?: boolean, + ) { + this.config = { table, values: values as any, withList, select } + } + + returning(): SQLiteEffectInsertReturningAll + returning( + fields: TSelectedFields, + ): SQLiteEffectInsertReturning + returning( + fields: SelectedFieldsFlat = getTableColumnsRuntime(this.config.table), + ): SQLiteEffectInsertWithout { + this.config.returning = orderSelectedFields(fields) + return this as any + } + + onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this { + if (!this.config.onConflict) this.config.onConflict = [] + + if (config.target === undefined) { + this.config.onConflict.push(sql` on conflict do nothing`) + return this + } + + const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}` + const whereSql = config.where ? sql` where ${config.where}` : sql`` + this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`) + return this + } + + onConflictDoUpdate(config: SQLiteEffectInsertOnConflictDoUpdateConfig): this { + if (config.where && (config.targetWhere || config.setWhere)) { + throw new Error( + 'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.', + ) + } + + if (!this.config.onConflict) this.config.onConflict = [] + + const whereSql = config.where ? sql` where ${config.where}` : undefined + const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined + const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined + const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}` + const setSql = this.effectDialect.buildUpdateSet( + this.config.table, + mapUpdateSet(this.config.table, config.set as SQLiteUpdateSetSource), + ) + this.config.onConflict.push( + sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`, + ) + return this + } + + /** @internal */ + getSQL(): SQL { + return this.effectDialect.buildInsertQuery(this.config) + } + + toSQL(): Query { + return this.effectDialect.sqlToQuery(this.getSQL()) + } + + /** @internal */ + _prepare(isOneTimeQuery = true): SQLiteEffectInsertPrepare { + return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + this.effectDialect.sqlToQuery(this.getSQL()), + this.config.returning, + this.config.returning ? "all" : "run", + undefined, + { + type: "insert", + tables: extractUsedTable(this.config.table), + }, + ) as SQLiteEffectInsertPrepare + } + + prepare(): SQLiteEffectInsertPrepare { + return this._prepare(false) + } + + run: ReturnType["run"] = (placeholderValues) => { + return this._prepare().run(placeholderValues) + } + + all: ReturnType["all"] = (placeholderValues) => { + return this._prepare().all(placeholderValues) + } + + get: ReturnType["get"] = (placeholderValues) => { + return this._prepare().get(placeholderValues) + } + + values: ReturnType["values"] = (placeholderValues) => { + return this._prepare().values(placeholderValues) + } + + execute: ReturnType["execute"] = (placeholderValues) => { + return this._prepare().execute(placeholderValues) + } + + $dynamic(): SQLiteEffectInsertDynamic { + return this as any + } +} + +applyEffectWrapper(SQLiteEffectInsertBase) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts new file mode 100644 index 00000000000..4676039e550 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts @@ -0,0 +1,198 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import { + type BuildQueryResult, + type BuildRelationalQueryResult, + type DBQueryConfig, + makeDefaultRqbMapper, + type TableRelationalConfig, + type TablesRelationalConfig, +} from "drizzle-orm/relations" +import type { RunnableQuery } from "drizzle-orm/runnable-query" +import { type Query, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql" +import type { KnownKeysOnly } from "drizzle-orm/utils" +import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" +import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" +import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + +export class SQLiteEffectRelationalQueryBuilder< + TSchema extends TablesRelationalConfig, + TFields extends TableRelationalConfig, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> { + static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2" + + constructor( + private schema: TSchema, + private table: SQLiteTable, + private tableConfig: TableRelationalConfig, + private dialect: SQLiteDialect, + private session: SQLiteEffectSession, + private rowMode?: boolean, + private forbidJsonb?: boolean, + ) {} + + findMany>( + config?: KnownKeysOnly>, + ): SQLiteEffectRelationalQuery[], TEffectHKT> { + return new SQLiteEffectRelationalQuery( + this.schema, + this.table, + this.tableConfig, + this.dialect, + this.session, + (config as DBQueryConfig<"many"> | undefined) ?? true, + "many", + this.rowMode, + this.forbidJsonb, + ) + } + + findFirst>( + config?: KnownKeysOnly>, + ): SQLiteEffectRelationalQuery | undefined, TEffectHKT> { + return new SQLiteEffectRelationalQuery( + this.schema, + this.table, + this.tableConfig, + this.dialect, + this.session, + (config as DBQueryConfig<"one"> | undefined) ?? true, + "first", + this.rowMode, + this.forbidJsonb, + ) + } +} + +export interface SQLiteEffectRelationalQuery + extends Effect.Effect, + RunnableQuery, + SQLWrapper {} + +export class SQLiteEffectRelationalQuery + implements RunnableQuery, SQLWrapper +{ + static readonly [entityKind]: string = "SQLiteEffectRelationalQueryV2" + + declare readonly _: { + readonly dialect: "sqlite" + readonly type: "async" + readonly result: TResult + } + + /** @internal */ + mode: "many" | "first" + /** @internal */ + table: SQLiteTable + + constructor( + private schema: TablesRelationalConfig, + table: SQLiteTable, + private tableConfig: TableRelationalConfig, + private dialect: SQLiteDialect, + private session: SQLiteEffectSession, + private config: DBQueryConfig<"many" | "one"> | true, + mode: "many" | "first", + private rowMode?: boolean, + private forbidJsonb?: boolean, + ) { + this.mode = mode + this.table = table + } + + /** @internal */ + getSQL(): SQL { + return this._getQuery().sql + } + + /** @internal */ + _prepare( + isOneTimeQuery = true, + ): SQLiteEffectPreparedQuery< + PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + TEffectHKT, + true + > { + const { query, builtQuery } = this._toSQL() + const mapperConfig = { + isFirst: this.mode === "first", + parseJson: !this.rowMode, + parseJsonIfString: false, + rootJsonMappers: true, + selection: query.selection, + } + + return this.session[isOneTimeQuery ? "prepareOneTimeRelationalQuery" : "prepareRelationalQuery"]( + builtQuery, + undefined, + this.mode === "first" ? "get" : "all", + makeDefaultRqbMapper(mapperConfig), + mapperConfig, + ) as SQLiteEffectPreparedQuery< + PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + TEffectHKT, + true + > + } + + prepare(): SQLiteEffectPreparedQuery< + PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult }, + TEffectHKT, + true + > { + return this._prepare(false) + } + + private _getQuery() { + const jsonb = this.forbidJsonb ? sql`json` : sql`jsonb` + + const query = this.dialect.buildRelationalQuery({ + schema: this.schema, + table: this.table, + tableConfig: this.tableConfig, + queryConfig: this.config, + mode: this.mode, + isNested: this.rowMode, + jsonb, + }) + + if (this.rowMode) { + const jsonColumns = sql.join( + query.selection.map((s) => { + return sql`${sql.raw(this.dialect.escapeString(s.key))}, ${ + s.selection ? sql`${jsonb}(${sql.identifier(s.key)})` : sql.identifier(s.key) + }` + }), + sql`, `, + ) + + query.sql = sql`select json_object(${jsonColumns}) as ${sql.identifier("r")} from (${query.sql}) as ${sql.identifier( + "t", + )}` + } + + return query + } + + private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } { + const query = this._getQuery() + + const builtQuery = this.dialect.sqlToQuery(query.sql) + + return { query, builtQuery } + } + + toSQL(): Query { + return this._toSQL().builtQuery + } + + execute(placeholderValues?: Record) { + return this.mode === "first" ? this._prepare().get(placeholderValues) : this._prepare().all(placeholderValues) + } +} + +applyEffectWrapper(SQLiteEffectRelationalQuery) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts new file mode 100644 index 00000000000..26e3a63d83c --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts @@ -0,0 +1,49 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind } from "drizzle-orm/entity" +import type { RunnableQuery } from "drizzle-orm/runnable-query" +import type { PreparedQuery } from "drizzle-orm/session" +import type { Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" + +type SQLiteEffectRawAction = "all" | "get" | "values" | "run" + +export interface SQLiteEffectRaw + extends Effect.Effect, + RunnableQuery, + SQLWrapper {} + +export class SQLiteEffectRaw + implements RunnableQuery, SQLWrapper, PreparedQuery +{ + static readonly [entityKind]: string = "SQLiteEffectRaw" + + declare readonly _: { + readonly dialect: "sqlite" + readonly result: TResult + } + + constructor( + public execute: () => Effect.Effect, + /** @internal */ + public getSQL: () => SQL, + private action: SQLiteEffectRawAction, + private dialect: SQLiteAsyncDialect, + private mapBatchResult: (result: unknown) => unknown, + ) {} + + getQuery(): Query & { method: SQLiteEffectRawAction } { + return { ...this.dialect.sqlToQuery(this.getSQL()), method: this.action } + } + + mapResult(result: unknown, isFromBatch?: boolean) { + return isFromBatch ? this.mapBatchResult(result) : result + } + + _prepare(): PreparedQuery { + return this + } +} + +applyEffectWrapper(SQLiteEffectRaw) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts new file mode 100644 index 00000000000..0cfde54331d --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts @@ -0,0 +1,279 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import type { CacheConfig } from "drizzle-orm/cache/core/types" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind, is } from "drizzle-orm/entity" +import type { + BuildSubquerySelection, + GetSelectTableName, + GetSelectTableSelection, + JoinNullability, + SelectMode, + SelectResult, +} from "drizzle-orm/query-builders/select.types" +import { SQL } from "drizzle-orm/sql/sql" +import type { ColumnsSelection, SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns" +import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" +import { SQLiteSelectQueryBuilderBase } from "drizzle-orm/sqlite-core/query-builders/select" +import type { + CreateSQLiteSelectFromBuilderMode, + SelectedFields, + SQLiteSelectConfig, + SQLiteSelectHKTBase, +} from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" +import { Subquery } from "drizzle-orm/subquery" +import { type Assume, getTableColumns } from "drizzle-orm/utils" +import { getViewSelectedFieldsRuntime, orderSelectedFields } from "../../internal/drizzle-utils" +import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + +export type SQLiteEffectSelectPrepare< + T extends AnySQLiteEffectSelect, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> = SQLiteEffectPreparedQuery< + { + type: "async" + run: T["_"]["runResult"] + all: T["_"]["result"] + get: T["_"]["result"][number] | undefined + values: any[][] + execute: T["_"]["result"] + }, + TEffectHKT +> + +export class SQLiteEffectSelectBuilder< + TSelection extends SelectedFields | undefined, + TRunResult, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + TBuilderMode extends "db" | "qb" = "db", +> { + static readonly [entityKind]: string = "SQLiteEffectSelectBuilder" + + private fields: TSelection + private session: SQLiteEffectSession | undefined + private dialect: SQLiteDialect + private withList: Subquery[] | undefined + private distinct: boolean | undefined + + constructor(config: { + fields: TSelection + session: SQLiteEffectSession | undefined + dialect: SQLiteDialect + withList?: Subquery[] + distinct?: boolean + }) { + this.fields = config.fields + this.session = config.session + this.dialect = config.dialect + this.withList = config.withList + this.distinct = config.distinct + } + + from( + source: TFrom, + ): TBuilderMode extends "db" + ? SQLiteEffectSelectBase< + GetSelectTableName, + TRunResult, + TSelection extends undefined ? GetSelectTableSelection : TSelection, + TSelection extends undefined ? "single" : "partial", + GetSelectTableName extends string ? Record, "not-null"> : {}, + false, + never, + SelectResult< + TSelection extends undefined ? GetSelectTableSelection : TSelection, + TSelection extends undefined ? "single" : "partial", + GetSelectTableName extends string ? Record, "not-null"> : {} + >[], + BuildSubquerySelection< + TSelection extends undefined ? GetSelectTableSelection : TSelection, + GetSelectTableName extends string ? Record, "not-null"> : {} + >, + TEffectHKT + > + : CreateSQLiteSelectFromBuilderMode< + TBuilderMode, + GetSelectTableName, + "async", + TRunResult, + TSelection extends undefined ? GetSelectTableSelection : TSelection, + TSelection extends undefined ? "single" : "partial" + > { + const isPartialSelect = !!this.fields + + let fields: SelectedFields + if (this.fields) { + fields = this.fields + } else if (is(source, Subquery)) { + fields = Object.fromEntries( + Object.keys(source._.selectedFields).map((key) => [ + key, + source[key as unknown as keyof typeof source] as unknown as SelectedFields[string], + ]), + ) + } else if (is(source, SQLiteViewBase)) { + fields = getViewSelectedFieldsRuntime(source).selectedFields as SelectedFields + } else if (is(source, SQL)) { + fields = {} + } else { + fields = getTableColumns(source) + } + + return new SQLiteEffectSelectBase({ + table: source, + fields, + isPartialSelect, + session: this.session as any, + dialect: this.dialect, + withList: this.withList, + distinct: this.distinct, + }) as any + } +} + +export interface SQLiteEffectSelectHKT + extends SQLiteSelectHKTBase { + _type: SQLiteEffectSelectBase< + this["tableName"], + this["runResult"], + Assume, + this["selectMode"], + Assume>, + this["dynamic"], + this["excludedMethods"], + Assume, + Assume, + TEffectHKT + > +} + +export interface SQLiteEffectSelectBase< + TTableName extends string | undefined, + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode = "single", + TNullabilityMap extends Record = TTableName extends string + ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> extends SQLiteSelectQueryBuilderBase< + SQLiteEffectSelectHKT, + TTableName, + "async", + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + >, + Effect.Effect {} + +export class SQLiteEffectSelectBase< + TTableName extends string | undefined, + TRunResult, + TSelection extends ColumnsSelection, + TSelectMode extends SelectMode = "single", + TNullabilityMap extends Record = TTableName extends string + ? Record + : {}, + TDynamic extends boolean = false, + TExcludedMethods extends string = never, + TResult extends any[] = SelectResult[], + TSelectedFields extends ColumnsSelection = BuildSubquerySelection, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + > + extends SQLiteSelectQueryBuilderBase< + SQLiteEffectSelectHKT, + TTableName, + "async", + TRunResult, + TSelection, + TSelectMode, + TNullabilityMap, + TDynamic, + TExcludedMethods, + TResult, + TSelectedFields + > + implements SQLWrapper +{ + static override readonly [entityKind]: string = "SQLiteEffectSelect" + + private get effectConfig() { + return (this as unknown as { config: SQLiteSelectConfig }).config + } + + /** @internal */ + getSQL(): SQL { + return this.dialect.buildSelectQuery(this.effectConfig) + } + + /** @internal */ + _prepare(isOneTimeQuery = true): SQLiteEffectSelectPrepare { + if (!this.session) { + throw new Error("Cannot execute a query on a query builder. Please use a database instance instead.") + } + const session = this.session as unknown as SQLiteEffectSession + const query = session[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + this.dialect.sqlToQuery(this.getSQL()), + orderSelectedFields(this.effectConfig.fields), + "all", + undefined, + { + type: "select", + tables: [...this.usedTables], + }, + this.cacheConfig, + ) + query.joinsNotNullableMap = this.joinsNotNullableMap + return query as ReturnType + } + + $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) { + this.cacheConfig = + config === undefined + ? { config: {}, enabled: true, autoInvalidate: true } + : config === false + ? { enabled: false } + : { enabled: true, autoInvalidate: true, ...config } + return this + } + + prepare(): SQLiteEffectSelectPrepare { + return this._prepare(false) + } + + run: ReturnType["run"] = (placeholderValues) => { + return this._prepare().run(placeholderValues) + } + + all: ReturnType["all"] = (placeholderValues) => { + return this._prepare().all(placeholderValues) + } + + get: ReturnType["get"] = (placeholderValues) => { + return this._prepare().get(placeholderValues) + } + + values: ReturnType["values"] = (placeholderValues) => { + return this._prepare().values(placeholderValues) + } + + execute: ReturnType["execute"] = (placeholderValues) => { + return this._prepare().execute(placeholderValues) + } +} + +applyEffectWrapper(SQLiteEffectSelectBase) + +export type AnySQLiteEffectSelect = SQLiteEffectSelectBase diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts new file mode 100644 index 00000000000..15a56f2ca7d --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts @@ -0,0 +1,490 @@ +/* oxlint-disable */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import type { SqlError } from "effect/unstable/sql/SqlError" +import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" +import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache" +import type { WithCacheConfig } from "drizzle-orm/cache/core/types" +import { MigratorInitError } from "drizzle-orm/effect-core/errors" +import { EffectDrizzleQueryError, EffectTransactionRollbackError } from "drizzle-orm/effect-core/errors" +import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger" +import type { QueryEffectHKTBase, QueryEffectKind } from "drizzle-orm/effect-core/query-effect" +import { entityKind, is } from "drizzle-orm/entity" +import type { MigrationConfig, MigrationMeta } from "drizzle-orm/migrator" +import { getMigrationsToRun } from "drizzle-orm/migrator.utils" +import type { + AnyRelations, + EmptyRelations, + RelationalQueryMapperConfig, + RelationalRowsMapper, +} from "drizzle-orm/relations" +import { makeJitRqbMapper } from "drizzle-orm/relations" +import type { PreparedQuery } from "drizzle-orm/session" +import { fillPlaceholders, type Query, type SQL, sql } from "drizzle-orm/sql/sql" +import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect" +import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" +import { upgradeIfNeeded } from "../../up-migrations/effect-sqlite" +import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils" +import { mapResultRow } from "../../internal/drizzle-utils" +import { SQLiteEffectDatabase } from "./db" + +type MigrationConfigWithInit = MigrationConfig & { init?: boolean } + +type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values" + +export class SQLiteEffectPreparedQuery< + T extends PreparedQueryConfig, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + TIsRqbV2 extends boolean = false, +> implements PreparedQuery +{ + static readonly [entityKind]: string = "SQLiteEffectPreparedQuery" + + /** @internal */ + joinsNotNullableMap?: Record + private jitMapper?: RowsMapper | RelationalRowsMapper + private cacheConfig: WithCacheConfig | undefined + private effectExecuteMethod: SQLiteExecuteMethod + + constructor( + private executor: ( + params: unknown[], + executeMethod: SQLiteEffectExecuteMethod, + ) => Effect.Effect, + protected query: Query, + private logger: EffectLoggerShape, + private cache: EffectCacheShape, + private queryMetadata: + | { + type: "select" | "update" | "delete" | "insert" + tables: string[] + } + | undefined, + cacheConfig: WithCacheConfig | undefined, + private fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + private useJitMappers: boolean | undefined, + private customResultMapper?: ( + rows: TIsRqbV2 extends true ? Record[] : unknown[][], + mapColumnValue?: (value: unknown) => unknown, + ) => unknown, + private isRqbV2Query?: TIsRqbV2, + private rqbConfig?: RelationalQueryMapperConfig, + private isInTransaction: Effect.Effect = Effect.succeed(false), + ) { + this.effectExecuteMethod = executeMethod + this.cacheConfig = + cache.strategy() === "all" && cacheConfig === undefined ? { enabled: true, autoInvalidate: true } : cacheConfig + if (!this.cacheConfig?.enabled) { + this.cacheConfig = undefined + } + } + + run(placeholderValues?: Record): QueryEffectKind + run(placeholderValues?: Record): any { + return this.executeWithCache(placeholderValues, "run") + } + + all(placeholderValues?: Record): QueryEffectKind + all(placeholderValues?: Record): any { + if (this.isRqbV2Query) return this.allRqbV2(placeholderValues) + + if (!this.fields && !this.customResultMapper) { + return this.executeWithCache(placeholderValues, "all") + } + + return this.executeWithCache( + placeholderValues, + "values", + (rows) => this.mapAllResult(rows) as T["all"], + ) + } + + get(placeholderValues?: Record): QueryEffectKind + get(placeholderValues?: Record): any { + if (this.isRqbV2Query) return this.getRqbV2(placeholderValues) + + if (!this.fields && !this.customResultMapper) { + return this.executeWithCache(placeholderValues, "get") + } + + return this.executeWithCache( + placeholderValues, + "values", + (rows) => this.mapGetResult(rows) as T["get"], + ) + } + + values(placeholderValues?: Record): QueryEffectKind + values(placeholderValues?: Record): any { + return this.executeWithCache(placeholderValues, "values") + } + + execute(placeholderValues?: Record): QueryEffectKind + execute(placeholderValues?: Record): any { + return this[this.effectExecuteMethod](placeholderValues) as QueryEffectKind + } + + mapRunResult(result: unknown, _isFromBatch?: boolean): unknown { + return result + } + + mapAllResult(rows: unknown, isFromBatch?: boolean): unknown { + if (isFromBatch) { + rows = Array.isArray(rows) ? rows : [] + } + + if (!this.fields && !this.customResultMapper) { + return rows + } + + if (this.isRqbV2Query) { + return this.useJitMappers + ? (this.jitMapper = + (this.jitMapper as RelationalRowsMapper) ?? makeJitRqbMapper(this.rqbConfig!))( + rows as Record[], + ) + : (this.customResultMapper as (rows: Record[]) => unknown)(rows as Record[]) + } + + if (this.customResultMapper) { + return (this.customResultMapper as (rows: unknown[][]) => unknown)(rows as unknown[][]) as T["all"] + } + + return this.useJitMappers + ? (this.jitMapper = + (this.jitMapper as RowsMapper) ?? + makeJitQueryMapper(this.fields!, this.joinsNotNullableMap))(rows as unknown[][]) + : (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap)) + } + + mapGetResult(rows: unknown, isFromBatch?: boolean): unknown { + if (isFromBatch) { + rows = Array.isArray(rows) ? rows : [] + } + + if (!this.fields && !this.customResultMapper) { + return Array.isArray(rows) ? rows[0] : rows + } + + const row = Array.isArray(rows) ? rows[0] : rows + if (!row) return undefined + + if (this.isRqbV2Query) { + return this.useJitMappers + ? (this.jitMapper = + (this.jitMapper as RelationalRowsMapper) ?? makeJitRqbMapper(this.rqbConfig!))([ + row as Record, + ]) + : (this.customResultMapper as (rows: Record[]) => unknown)([row as Record]) + } + + if (this.customResultMapper) { + return (this.customResultMapper as (rows: unknown[][]) => unknown)([row as unknown[]]) as T["get"] + } + + return this.useJitMappers + ? (this.jitMapper = + (this.jitMapper as RowsMapper) ?? + makeJitQueryMapper(this.fields!, this.joinsNotNullableMap))([row as unknown[]])[0] + : mapResultRow(this.fields!, row as unknown[], this.joinsNotNullableMap) + } + + private allRqbV2(placeholderValues?: Record) { + return this.executeWithCache( + placeholderValues, + "all", + (rows) => this.mapAllResult(rows) as T["all"], + ) + } + + private getRqbV2(placeholderValues?: Record) { + return this.executeWithCache(placeholderValues, "get", (row) => + row === undefined ? undefined : (this.mapGetResult(row) as T["get"]), + ) + } + + private executeWithCache( + placeholderValues: Record | undefined, + executeMethod: SQLiteEffectExecuteMethod, + mapResult?: (result: A) => B, + ) { + return Effect.gen({ self: this }, function* () { + const params = fillPlaceholders(this.query.params, placeholderValues ?? {}) + + yield* this.logger.logQuery(this.query.sql, params) + + return yield* this.queryWithCache( + this.query.sql, + params, + Effect.suspend(() => this.executor(params, executeMethod) as Effect.Effect), + mapResult, + ) + }) + } + + private mapCachedResult(result: A, mapResult: ((result: A) => B) | undefined) { + if (!mapResult) return Effect.succeed(result as unknown as B) + return Effect.try({ + try: () => mapResult(result), + catch: (cause) => cause, + }) + } + + private queryWithCache( + queryString: string, + params: unknown[], + query: Effect.Effect, + mapResult?: (result: A) => B, + ) { + return Effect.gen({ self: this }, function* () { + if (this.queryMetadata?.type === "select" && this.cacheConfig?.enabled && (yield* this.isInTransaction)) { + return yield* this.mapCachedResult(yield* query, mapResult) + } + + const cacheStrat: Awaited> = !is(this.cache.cache, NoopCache) + ? yield* Effect.tryPromise(() => strategyFor(queryString, params, this.queryMetadata, this.cacheConfig)) + : { type: "skip" as const } + + if (cacheStrat.type === "skip") { + return yield* this.mapCachedResult(yield* query, mapResult) + } + + if (cacheStrat.type === "invalidate") { + const result = yield* query + yield* this.cache.onMutate({ tables: cacheStrat.tables }) + return yield* this.mapCachedResult(result, mapResult) + } + + if (cacheStrat.type === "try") { + if (yield* this.isInTransaction) { + return yield* this.mapCachedResult(yield* query, mapResult) + } + + const { tables, key, isTag, autoInvalidate, config } = cacheStrat + const fromCache: any[] | undefined = yield* this.cache.get(key, tables, isTag, autoInvalidate) + + if (typeof fromCache !== "undefined") { + return yield* this.mapCachedResult(fromCache as unknown as A, mapResult) + } + + const result = yield* query + + yield* this.cache.put(key, result, autoInvalidate ? tables : [], isTag, config) + + return yield* this.mapCachedResult(result, mapResult) + } + + assertUnreachable(cacheStrat) + }).pipe( + Effect.catch((e) => { + return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) })) + }), + ) + } + + getQuery(): Query { + return this.query + } + + mapResult(response: unknown, isFromBatch?: boolean) { + switch (this.effectExecuteMethod) { + case "run": { + return this.mapRunResult(response, isFromBatch) + } + case "all": { + return this.mapAllResult(response, isFromBatch) + } + case "get": { + return this.mapGetResult(response, isFromBatch) + } + } + } +} + +export abstract class SQLiteEffectSession< + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + TRunResult = unknown, + TRelations extends AnyRelations = EmptyRelations, +> { + static readonly [entityKind]: string = "SQLiteEffectSession" + + constructor(readonly dialect: SQLiteAsyncDialect) {} + + abstract prepareQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + queryMetadata?: { + type: "select" | "update" | "delete" | "insert" + tables: string[] + }, + cacheConfig?: WithCacheConfig, + ): SQLiteEffectPreparedQuery + + prepareOneTimeQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown, + queryMetadata?: { + type: "select" | "update" | "delete" | "insert" + tables: string[] + }, + cacheConfig?: WithCacheConfig, + ): SQLiteEffectPreparedQuery { + return this.prepareQuery(query, fields, executeMethod, customResultMapper, queryMetadata, cacheConfig) + } + + abstract prepareRelationalQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper: (rows: Record[], mapColumnValue?: (value: unknown) => unknown) => unknown, + config: RelationalQueryMapperConfig, + ): SQLiteEffectPreparedQuery + + prepareOneTimeRelationalQuery( + query: Query, + fields: SelectedFieldsOrdered | undefined, + executeMethod: SQLiteExecuteMethod, + customResultMapper: (rows: Record[], mapColumnValue?: (value: unknown) => unknown) => unknown, + config: RelationalQueryMapperConfig, + ): SQLiteEffectPreparedQuery { + return this.prepareRelationalQuery(query, fields, executeMethod, customResultMapper, config) + } + + run(query: SQL): QueryEffectKind + run(query: SQL): any { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + "run", + ).run() + } + + all(query: SQL): QueryEffectKind + all(query: SQL): any { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + "all", + ).all() + } + + get(query: SQL): QueryEffectKind + get(query: SQL): any { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + "get", + ).get() + } + + values(query: SQL): QueryEffectKind + values(query: SQL): any { + return this.prepareQuery( + this.dialect.sqlToQuery(query), + undefined, + "all", + ).values() + } + + count(query: SQL): QueryEffectKind + count(query: SQL): any { + return this.values<[number]>(query).pipe(Effect.map((result) => result[0]?.[0] ?? 0)) + } + + abstract transaction( + transaction: (tx: SQLiteEffectTransaction) => Effect.Effect, + config?: SQLiteTransactionConfig, + ): Effect.Effect +} + +export abstract class SQLiteEffectTransaction< + TEffectHKT extends QueryEffectHKTBase, + TRunResult, + TRelations extends AnyRelations = EmptyRelations, +> extends SQLiteEffectDatabase { + static override readonly [entityKind]: string = "SQLiteEffectTransaction" + + constructor( + dialect: SQLiteAsyncDialect, + session: SQLiteEffectSession, + protected relations: TRelations, + ) { + super(dialect, session, relations) + } + + rollback() { + return new EffectTransactionRollbackError() + } +} + +export const migrate = Effect.fn("migrate")(function* ( + migrations: MigrationMeta[], + session: SQLiteEffectSession, + config: string | MigrationConfigWithInit, +) { + const migrationsTable = + typeof config === "string" ? "__drizzle_migrations" : (config.migrationsTable ?? "__drizzle_migrations") + + const { newDb } = yield* upgradeIfNeeded(migrationsTable, session, migrations) + + if (newDb) { + yield* session.run(sql` + CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} ( + id INTEGER PRIMARY KEY, + hash text NOT NULL, + created_at numeric, + name text, + applied_at TEXT + ) + `) + } + + const dbMigrations = yield* session.all<{ id: number; hash: string; created_at: string; name: string | null }>( + sql`SELECT id, hash, created_at, name FROM ${sql.identifier(migrationsTable)}`, + ) + + if (typeof config === "object" && config.init) { + if (dbMigrations.length) { + return yield* new MigratorInitError({ exitCode: "databaseMigrations" }) + } + + if (migrations.length > 1) { + return yield* new MigratorInitError({ exitCode: "localMigrations" }) + } + + const [migration] = migrations + if (!migration) return + + yield* session.run( + sql`insert into ${sql.identifier( + migrationsTable, + )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, + ) + + return + } + + const migrationsToRun = getMigrationsToRun({ localMigrations: migrations, dbMigrations }) + if (migrationsToRun.length === 0) return + + yield* session.transaction((tx) => + Effect.gen(function* () { + for (const migration of migrationsToRun) { + for (const stmt of migration.sql) { + yield* tx.run(sql.raw(stmt)) + } + yield* tx.run( + sql`insert into ${sql.identifier( + migrationsTable, + )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, + ) + } + }), + ) +}) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts new file mode 100644 index 00000000000..369e08e878e --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts @@ -0,0 +1,402 @@ +/* oxlint-disable */ +import type * as Effect from "effect/Effect" +import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import { entityKind, is } from "drizzle-orm/entity" +import type { SelectResultFields } from "drizzle-orm/query-builders/select.types" +import type { RunnableQuery } from "drizzle-orm/runnable-query" +import { SelectionProxyHandler } from "drizzle-orm/selection-proxy" +import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql" +import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" +import type { SelectedFields, SQLiteSelectJoinConfig } from "drizzle-orm/sqlite-core/query-builders/select.types" +import type { SQLiteUpdateConfig, SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update" +import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session" +import { SQLiteTable } from "drizzle-orm/sqlite-core/table" +import { extractUsedTable } from "drizzle-orm/sqlite-core/utils" +import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base" +import { Subquery } from "drizzle-orm/subquery" +import { type DrizzleTypeError, type UpdateSet, type ValueOrArray } from "drizzle-orm/utils" +import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common" +import { + getTableColumnsRuntime, + getTableLikeName, + getViewSelectedFieldsRuntime, + mapUpdateSet, + orderSelectedFields, +} from "../../internal/drizzle-utils" +import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session" + +export type SQLiteEffectUpdateWithout< + T extends AnySQLiteEffectUpdate, + TDynamic extends boolean, + K extends keyof T & string, +> = TDynamic extends true + ? T + : Omit< + SQLiteEffectUpdateBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["from"], + T["_"]["returning"], + TDynamic, + T["_"]["excludedMethods"] | K, + T["_"]["effectHKT"] + >, + T["_"]["excludedMethods"] | K + > + +export type SQLiteEffectUpdateWithJoins< + T extends AnySQLiteEffectUpdate, + TDynamic extends boolean, + TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL, +> = TDynamic extends true + ? T + : Omit< + SQLiteEffectUpdateBase< + T["_"]["table"], + T["_"]["runResult"], + TFrom, + T["_"]["returning"], + TDynamic, + Exclude, + T["_"]["effectHKT"] + >, + Exclude + > + +export type SQLiteEffectUpdateReturningAll< + T extends AnySQLiteEffectUpdate, + TDynamic extends boolean, +> = SQLiteEffectUpdateWithout< + SQLiteEffectUpdateBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["from"], + T["_"]["table"]["$inferSelect"], + TDynamic, + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectUpdateReturning< + T extends AnySQLiteEffectUpdate, + TDynamic extends boolean, + TSelectedFields extends SelectedFields, +> = SQLiteEffectUpdateWithout< + SQLiteEffectUpdateBase< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["from"], + SelectResultFields, + TDynamic, + T["_"]["excludedMethods"], + T["_"]["effectHKT"] + >, + TDynamic, + "returning" +> + +export type SQLiteEffectUpdateExecute = T["_"]["returning"] extends undefined + ? T["_"]["runResult"] + : T["_"]["returning"][] + +export type SQLiteEffectUpdatePrepare< + T extends AnySQLiteEffectUpdate, + TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"], +> = SQLiteEffectPreparedQuery< + PreparedQueryConfig & { + run: T["_"]["runResult"] + all: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".all() cannot be used without .returning()"> + : T["_"]["returning"][] + get: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".get() cannot be used without .returning()"> + : T["_"]["returning"] + values: T["_"]["returning"] extends undefined + ? DrizzleTypeError<".values() cannot be used without .returning()"> + : any[][] + execute: SQLiteEffectUpdateExecute + }, + TEffectHKT +> + +export type SQLiteEffectUpdateDynamic = SQLiteEffectUpdate< + T["_"]["table"], + T["_"]["runResult"], + T["_"]["from"], + T["_"]["returning"], + T["_"]["effectHKT"] +> + +export type SQLiteEffectUpdate< + TTable extends SQLiteTable = SQLiteTable, + TRunResult = unknown, + TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + TReturning extends Record | undefined = Record | undefined, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> = SQLiteEffectUpdateBase + +export type AnySQLiteEffectUpdate = SQLiteEffectUpdateBase + +export type SQLiteEffectUpdateJoinFn = < + TJoinedTable extends SQLiteTable | Subquery | SQLiteViewBase | SQL, +>( + table: TJoinedTable, + on: + | (( + updateTable: T["_"]["table"]["_"]["columns"], + from: T["_"]["from"] extends SQLiteTable + ? T["_"]["from"]["_"]["columns"] + : T["_"]["from"] extends Subquery | SQLiteViewBase + ? T["_"]["from"]["_"]["selectedFields"] + : never, + ) => SQL | undefined) + | SQL + | undefined, +) => T + +export class SQLiteEffectUpdateBuilder< + TTable extends SQLiteTable, + TRunResult, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> { + static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder" + + declare readonly _: { + readonly table: TTable + } + + constructor( + protected table: TTable, + protected session: SQLiteEffectSession, + protected dialect: SQLiteDialect, + private withList?: Subquery[], + ) {} + + set( + values: SQLiteUpdateSetSource, + ): SQLiteEffectUpdateWithout< + SQLiteEffectUpdateBase, + false, + "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin" + > { + return new SQLiteEffectUpdateBase( + this.table, + mapUpdateSet(this.table, values), + this.session, + this.dialect, + this.withList, + ) as any + } +} + +export interface SQLiteEffectUpdateBase< + TTable extends SQLiteTable = SQLiteTable, + TRunResult = unknown, + TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + TReturning = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, +> extends SQLWrapper, + RunnableQuery, + Effect.Effect< + TReturning extends undefined ? TRunResult : TReturning[], + TEffectHKT["error"], + TEffectHKT["context"] + > { + readonly _: { + readonly dialect: "sqlite" + readonly table: TTable + readonly resultType: "async" + readonly runResult: TRunResult + readonly from: TFrom + readonly returning: TReturning + readonly dynamic: TDynamic + readonly excludedMethods: _TExcludedMethods + readonly result: TReturning extends undefined ? TRunResult : TReturning[] + readonly effectHKT: TEffectHKT + } +} + +export class SQLiteEffectUpdateBase< + TTable extends SQLiteTable = SQLiteTable, + TRunResult = unknown, + TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined, + TReturning = undefined, + TDynamic extends boolean = false, + _TExcludedMethods extends string = never, + TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase, + > + implements RunnableQuery, SQLWrapper +{ + static readonly [entityKind]: string = "SQLiteEffectUpdate" + + /** @internal */ + config: SQLiteUpdateConfig + + constructor( + table: TTable, + set: UpdateSet, + private effectSession: SQLiteEffectSession, + private effectDialect: SQLiteDialect, + withList?: Subquery[], + ) { + this.config = { set, table, withList, joins: [] } + } + + from( + source: TFrom, + ): SQLiteEffectUpdateWithJoins { + this.config.from = source + return this as any + } + + private createJoin( + joinType: TJoinType, + ): SQLiteEffectUpdateJoinFn { + return (( + table: SQLiteTable | Subquery | SQLiteViewBase | SQL, + on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined, + ) => { + const tableName = getTableLikeName(table) + + if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) { + throw new Error(`Alias "${tableName}" is already used in this query`) + } + + if (typeof on === "function") { + const from = this.config.from + ? is(table, SQLiteTable) + ? getTableColumnsRuntime(table) + : is(table, Subquery) + ? table._.selectedFields + : is(table, SQLiteViewBase) + ? getViewSelectedFieldsRuntime(table).selectedFields + : undefined + : undefined + on = on( + new Proxy( + this.config.table._.columns, + new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }), + ) as any, + from && + (new Proxy(from, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" })) as any), + ) + } + + this.config.joins.push({ on, table, joinType, alias: tableName }) + + return this as any + }) as any + } + + leftJoin = this.createJoin("left") + + rightJoin = this.createJoin("right") + + innerJoin = this.createJoin("inner") + + fullJoin = this.createJoin("full") + + where(where: SQL | undefined): SQLiteEffectUpdateWithout { + this.config.where = where + return this as any + } + + orderBy( + builder: (updateTable: TTable) => ValueOrArray, + ): SQLiteEffectUpdateWithout + orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectUpdateWithout + orderBy( + ...columns: + | [(updateTable: TTable) => ValueOrArray] + | (SQLiteColumn | SQL | SQL.Aliased)[] + ): SQLiteEffectUpdateWithout { + if (typeof columns[0] === "function") { + const orderBy = columns[0]( + new Proxy( + getTableColumnsRuntime(this.config.table), + new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }), + ) as any, + ) + + this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy] + return this as any + } + + this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[] + return this as any + } + + limit(limit: number | Placeholder): SQLiteEffectUpdateWithout { + this.config.limit = limit + return this as any + } + + returning(): SQLiteEffectUpdateReturningAll + returning( + fields: TSelectedFields, + ): SQLiteEffectUpdateReturning + returning( + fields: SelectedFields = getTableColumnsRuntime(this.config.table), + ): SQLiteEffectUpdateWithout { + this.config.returning = orderSelectedFields(fields) + return this as any + } + + /** @internal */ + getSQL(): SQL { + return this.effectDialect.buildUpdateQuery(this.config) + } + + toSQL(): Query { + return this.effectDialect.sqlToQuery(this.getSQL()) + } + + /** @internal */ + _prepare(isOneTimeQuery = true): SQLiteEffectUpdatePrepare { + return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"]( + this.effectDialect.sqlToQuery(this.getSQL()), + this.config.returning, + this.config.returning ? "all" : "run", + undefined, + { + type: "update", + tables: extractUsedTable(this.config.table), + }, + ) as SQLiteEffectUpdatePrepare + } + + prepare(): SQLiteEffectUpdatePrepare { + return this._prepare(false) + } + + run: ReturnType["run"] = (placeholderValues) => { + return this._prepare().run(placeholderValues) + } + + all: ReturnType["all"] = (placeholderValues) => { + return this._prepare().all(placeholderValues) + } + + get: ReturnType["get"] = (placeholderValues) => { + return this._prepare().get(placeholderValues) + } + + values: ReturnType["values"] = (placeholderValues) => { + return this._prepare().values(placeholderValues) + } + + execute: ReturnType["execute"] = (placeholderValues) => { + return this._prepare().execute(placeholderValues) + } + + $dynamic(): SQLiteEffectUpdateDynamic { + return this as any + } +} + +applyEffectWrapper(SQLiteEffectUpdateBase) diff --git a/packages/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts b/packages/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts new file mode 100644 index 00000000000..3418752271e --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts @@ -0,0 +1,102 @@ +/* oxlint-disable */ +import * as Effect from "effect/Effect" +import type { SqlError } from "effect/unstable/sql/SqlError" +import { EffectDrizzleError } from "drizzle-orm/effect-core/errors" +import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" +import type { MigrationMeta } from "drizzle-orm/migrator" +import { sql } from "drizzle-orm/sql/sql" +import type { SQLiteEffectSession } from "../sqlite-core/effect/session" +import { + buildSQLiteMigrationBackfillStatements, + prepareSQLiteMigrationBackfill, + type SQLiteMigrationTableRow, +} from "./sqlite" +import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils" + +const migrationUpgradeError = (cause: unknown) => + new EffectDrizzleError({ + message: + typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string" + ? cause.message + : String(cause), + cause, + }) + +export const upgradeIfNeeded: ( + migrationsTable: string, + session: SQLiteEffectSession, + localMigrations: MigrationMeta[], +) => Effect.Effect = + Effect.fn("upgradeIfNeeded")(function* ( + migrationsTable: string, + session: SQLiteEffectSession, + localMigrations: MigrationMeta[], + ) { + const tableExists = yield* session.all( + sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`, + ) + + if (tableExists.length === 0) { + return { newDb: true } + } + + const rows = yield* session.all<{ column_name: string }>( + sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + ) + + const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + + for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + const upgradeFn = upgradeFunctions[v] + if (!upgradeFn) { + return yield* new EffectDrizzleError({ + message: `No upgrade path from migration table version ${v} to ${v + 1}`, + cause: { version: v }, + }) + } + yield* upgradeFn(migrationsTable, session, localMigrations) + } + + return { newDb: false } + }) + +const upgradeFunctions: Record< + number, + ( + migrationsTable: string, + session: SQLiteEffectSession, + localMigrations: MigrationMeta[], + ) => Effect.Effect +> = { + 0: upgradeFromV0, +} + +function upgradeFromV0( + migrationsTable: string, + session: SQLiteEffectSession, + localMigrations: MigrationMeta[], +): Effect.Effect { + return Effect.gen(function* () { + const table = sql`${sql.identifier(migrationsTable)}` + + const dbRows = yield* session.all( + sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`, + ) + const statements = yield* Effect.try({ + try: () => + buildSQLiteMigrationBackfillStatements( + migrationsTable, + prepareSQLiteMigrationBackfill(dbRows, localMigrations), + ), + catch: migrationUpgradeError, + }) + + yield* session.transaction((tx) => + Effect.gen(function* () { + for (const statement of statements) { + yield* tx.run(statement) + } + }), + ) + }) +} diff --git a/packages/effect-drizzle-sqlite/src/up-migrations/sqlite.ts b/packages/effect-drizzle-sqlite/src/up-migrations/sqlite.ts new file mode 100644 index 00000000000..2887c66bed7 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/up-migrations/sqlite.ts @@ -0,0 +1,253 @@ +/* oxlint-disable */ +import type { TablesRelationalConfig } from "drizzle-orm/_relations" +import type { MigrationMeta } from "drizzle-orm/migrator" +import type { AnyRelations } from "drizzle-orm/relations" +import { type SQL, sql } from "drizzle-orm/sql/sql" +import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core" +import type { SQLiteSession } from "drizzle-orm/sqlite-core/session" +import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils" + +/** @internal */ +export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number } + +type AsyncSQLiteDatabaseWithSession = BaseSQLiteDatabase<"async", unknown, Record> & { + session: { + all(query: SQL): Promise + } + transaction(transaction: (tx: { run(query: SQL): Promise }) => Promise): Promise +} + +type SQLiteMigrationBackfillEntry = { + name: string + selector: + | { column: "id"; value: number } + | { column: "created_at"; value: number } + | { column: "hash"; value: string } +} + +function unmatchedMigrationError(unmatched: SQLiteMigrationTableRow[]) { + return new Error( + `While upgrading your database migrations table we found ${unmatched.length} (${unmatched + .map((it) => `[id: ${it.id}, created_at: ${it.created_at}]`) + .join( + ", ", + )}) migrations in the database that do not match any local migration. This means that some migrations were applied to the database but are missing from the local environment`, + ) +} + +/** @internal */ +export function prepareSQLiteMigrationBackfill( + dbRows: SQLiteMigrationTableRow[], + localMigrations: MigrationMeta[], +): SQLiteMigrationBackfillEntry[] { + const sortedLocalMigrations = [...localMigrations].sort((a, b) => + a.folderMillis !== b.folderMillis ? a.folderMillis - b.folderMillis : (a.name ?? "").localeCompare(b.name ?? ""), + ) + const byMillis = new Map() + const byHash = new Map() + for (const migration of sortedLocalMigrations) { + if (!byMillis.has(migration.folderMillis)) { + byMillis.set(migration.folderMillis, []) + } + byMillis.get(migration.folderMillis)!.push(migration) + byHash.set(migration.hash, migration) + } + + const toApply: SQLiteMigrationBackfillEntry[] = [] + const unmatched: SQLiteMigrationTableRow[] = [] + + for (const dbRow of dbRows) { + const stringified = String(dbRow.created_at) + const millis = Number(stringified.substring(0, stringified.length - 3) + "000") + const candidates = byMillis.get(millis) + + const matchedByMillis = candidates?.length === 1 ? candidates[0] : undefined + const matchedByCandidateHash = + candidates && candidates.length > 1 + ? candidates.find((candidate) => candidate.hash && dbRow.hash && candidate.hash === dbRow.hash) + : undefined + const matchedByHash = matchedByMillis || matchedByCandidateHash ? undefined : byHash.get(dbRow.hash) + const matched = matchedByMillis ?? matchedByCandidateHash ?? matchedByHash + + if (matched) { + toApply.push({ + name: matched.name, + selector: + dbRow.id !== null + ? { column: "id", value: dbRow.id } + : matchedByMillis + ? { column: "created_at", value: dbRow.created_at } + : { column: "hash", value: dbRow.hash }, + }) + continue + } + + unmatched.push(dbRow) + } + + if (unmatched.length > 0) { + throw unmatchedMigrationError(unmatched) + } + + return toApply +} + +/** @internal */ +export function buildSQLiteMigrationBackfillStatements( + migrationsTable: string, + backfillEntries: SQLiteMigrationBackfillEntry[], +) { + const table = sql`${sql.identifier(migrationsTable)}` + const statements: SQL[] = [ + sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("name")} text`, + sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("applied_at")} TEXT`, + ] + + for (const backfillEntry of backfillEntries) { + const updateQuery = sql`UPDATE ${table} SET ${sql.identifier("name")} = ${backfillEntry.name}, ${sql.identifier( + "applied_at", + )} = NULL WHERE` + + updateQuery.append(sql` ${sql.identifier(backfillEntry.selector.column)} = ${backfillEntry.selector.value}`) + + statements.push(updateQuery) + } + + return statements +} + +/** + * Detects the current version of the migrations table schema and upgrades it if needed. + * + * Version 0: Original schema (id, hash, created_at) + * Version 1: Extended schema (id, hash, created_at, name, applied_at) + */ +export function upgradeSyncIfNeeded( + migrationsTable: string, + session: SQLiteSession<"sync", unknown, Record, AnyRelations, TablesRelationalConfig>, + localMigrations: MigrationMeta[], +): UpgradeResult { + const tableExists = session.all(sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`) + + if (tableExists.length === 0) { + return { newDb: true } + } + + // Table exists, check table shape + const rows = session.all<{ column_name: string }>( + sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + ) + + const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + + for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + const upgradeFn = upgradeSyncFunctions[v] + if (!upgradeFn) { + throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`) + } + upgradeFn(migrationsTable, session, localMigrations) + } + + return { newDb: false } +} + +const upgradeSyncFunctions: Record< + number, + ( + migrationsTable: string, + session: SQLiteSession<"sync", unknown, Record, AnyRelations, TablesRelationalConfig>, + localMigrations: MigrationMeta[], + ) => void +> = { + /** + * Upgrade from version 0 to version 1: + * 1. Read all existing DB migrations + * 2. Sort localMigrations ASC by millis and if the same - sort by name + * 3. Match each DB row to a local migration + * If multiple migrations share the same second, use hash matching as a tiebreaker + * Not implemented for now -> If hash matching fails, fall back to serial id ordering + * 5. Create extra column and backfill names for matched migrations + */ + 0: (migrationsTable, session, localMigrations) => { + const table = sql`${sql.identifier(migrationsTable)}` + const dbRows = session.all(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`) + const statements = buildSQLiteMigrationBackfillStatements( + migrationsTable, + prepareSQLiteMigrationBackfill(dbRows, localMigrations), + ) + + session.transaction((tx) => { + for (const statement of statements) { + tx.run(statement) + } + }) + }, +} + +/** + * Detects the current version of the migrations table schema and upgrades it if needed. + * + * Version 0: Original schema (id, hash, created_at) + * Version 1: Extended schema (id, hash, created_at, name, applied_at) + */ +export async function upgradeAsyncIfNeeded( + migrationsTable: string, + db: AsyncSQLiteDatabaseWithSession, + localMigrations: MigrationMeta[], +): Promise { + // Check if the table exists at all + const tableExists = await db.session.all( + sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`, + ) + + if (tableExists.length === 0) { + return { newDb: true } + } + + const rows = await db.session.all<{ column_name: string }>( + sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, + ) + + const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) + + for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { + const upgradeFn = upgradeAsyncFunctions[v] + if (!upgradeFn) { + throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`) + } + await upgradeFn(migrationsTable, db, localMigrations) + } + + return { newDb: false } +} + +const upgradeAsyncFunctions: Record< + number, + (migrationsTable: string, db: AsyncSQLiteDatabaseWithSession, localMigrations: MigrationMeta[]) => Promise +> = { + /** + * Upgrade from version 0 to version 1: + * 1. Read all existing DB migrations + * 2. Sort localMigrations ASC by millis and if the same - sort by name + * 3. Match each DB row to a local migration + * If multiple migrations share the same second, use hash matching as a tiebreaker + * Not implemented for now -> If hash matching fails, fall back to serial id ordering + * 5. Create extra column and backfill names for matched migrations + */ + 0: async (migrationsTable, db, localMigrations) => { + const table = sql`${sql.identifier(migrationsTable)}` + const dbRows = await db.session.all( + sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`, + ) + const statements = buildSQLiteMigrationBackfillStatements( + migrationsTable, + prepareSQLiteMigrationBackfill(dbRows, localMigrations), + ) + + await db.transaction(async (tx) => { + for (const statement of statements) { + await tx.run(statement) + } + }) + }, +} diff --git a/packages/effect-drizzle-sqlite/src/up-migrations/utils.ts b/packages/effect-drizzle-sqlite/src/up-migrations/utils.ts new file mode 100644 index 00000000000..cc71213ac40 --- /dev/null +++ b/packages/effect-drizzle-sqlite/src/up-migrations/utils.ts @@ -0,0 +1,45 @@ +/* oxlint-disable */ +export interface UpgradeResult { + newDb: boolean +} + +export const MIGRATIONS_TABLE_VERSIONS = { + sqlite: 1, + pg: 1, + effect: 1, + mysql: 1, + mssql: 1, + cockroach: 1, + singlestore: 1, +} as const + +export const GET_VERSION_FOR = { + mysql: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + pg: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + effect: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + mssql: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + cockroach: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + singlestore: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, + sqlite: (columns: string[]): number => { + if (columns.includes("name")) return 1 + return 0 + }, +} as const diff --git a/packages/effect-drizzle-sqlite/sst-env.d.ts b/packages/effect-drizzle-sqlite/sst-env.d.ts new file mode 100644 index 00000000000..f25b9714550 --- /dev/null +++ b/packages/effect-drizzle-sqlite/sst-env.d.ts @@ -0,0 +1,10 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +/* biome-ignore-all lint: auto-generated */ + +/// + +import "sst" +export {} diff --git a/packages/effect-drizzle-sqlite/test/sqlite.test.ts b/packages/effect-drizzle-sqlite/test/sqlite.test.ts new file mode 100644 index 00000000000..69e6ebed2eb --- /dev/null +++ b/packages/effect-drizzle-sqlite/test/sqlite.test.ts @@ -0,0 +1,139 @@ +import { mkdir, mkdtemp, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { expect, test } from "bun:test" +import { SqliteClient } from "@effect/sql-sqlite-bun" +import { eq, sql } from "drizzle-orm" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { Effect } from "effect" +import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient" +import { EffectDrizzleSqlite } from "../src" + +const users = sqliteTable("users", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), +}) + +const run = (effect: Effect.Effect) => + Effect.runPromise( + effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped), + ) + +const makeDb = Effect.gen(function* () { + const db = yield* EffectDrizzleSqlite.makeWithDefaults() + yield* db.run(sql`create table users (id integer primary key autoincrement, name text not null)`) + return db +}) + +const createMigrationsFolder = async () => { + const migrationsFolder = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-")) + await mkdir(join(migrationsFolder, "20240101000000_create_migrated_users"), { recursive: true }) + await Bun.write( + join(migrationsFolder, "20240101000000_create_migrated_users", "migration.sql"), + "create table migrated_users (id integer primary key autoincrement, name text not null);", + ) + return migrationsFolder +} + +test("selects rows through Effect-yieldable query builders", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.insert(users).values({ name: "Ada" }) + + expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Ada" }]) + expect(yield* db.select({ id: users.id }).from(users).where(eq(users.name, "Ada")).get()).toEqual({ id: 1 }) + }), + ) +}) + +test("commits successful transactions", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + + yield* db.transaction((tx) => tx.insert(users).values({ name: "Grace" }), { behavior: "immediate" }) + + expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Grace" }]) + }), + ) +}) + +test("rolls back failed transactions", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + + yield* db + .transaction((tx) => + tx + .insert(users) + .values({ name: "Linus" }) + .pipe(Effect.andThen(Effect.fail("boom"))), + ) + .pipe(Effect.ignore) + + expect(yield* db.select().from(users)).toEqual([]) + }), + ) +}) + +test("rolls back explicit transaction rollback", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + + yield* db + .transaction((tx) => + tx + .insert(users) + .values({ name: "Barbara" }) + .pipe(Effect.andThen(Effect.fail(tx.rollback()))), + ) + .pipe(Effect.ignore) + + expect(yield* db.select().from(users)).toEqual([]) + }), + ) +}) + +test("supports returning and rejects empty update sets", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + + const inserted = yield* db.insert(users).values({ name: "Ada" }).returning({ id: users.id, name: users.name }) + expect(inserted).toEqual([{ id: 1, name: "Ada" }]) + + const updated = yield* db.update(users).set({ name: "Grace" }).where(eq(users.id, 1)).returning() + expect(updated).toEqual([{ id: 1, name: "Grace" }]) + + const deleted = yield* db.delete(users).where(eq(users.id, 1)).returning({ id: users.id }) + expect(deleted).toEqual([{ id: 1 }]) + + expect(() => db.update(users).set({ name: undefined })).toThrow("No values to set") + }), + ) +}) + +test("runs migrations once and records migration metadata", async () => { + const migrationsFolder = await createMigrationsFolder() + try { + await run( + Effect.gen(function* () { + const db = yield* EffectDrizzleSqlite.makeWithDefaults() + + yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder }) + yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder }) + yield* db.run(sql`insert into migrated_users (name) values ('Margaret')`) + + expect(yield* db.all<{ name: string }>(sql`select name from migrated_users`)).toEqual([{ name: "Margaret" }]) + expect(yield* db.all<{ name: string | null }>(sql`select name from __drizzle_migrations`)).toEqual([ + { name: "20240101000000_create_migrated_users" }, + ]) + }), + ) + } finally { + await rm(migrationsFolder, { recursive: true, force: true }) + } +}) diff --git a/packages/effect-drizzle-sqlite/tsconfig.json b/packages/effect-drizzle-sqlite/tsconfig.json new file mode 100644 index 00000000000..1d2432762da --- /dev/null +++ b/packages/effect-drizzle-sqlite/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["bun"], + "noUncheckedIndexedAccess": false, + "plugins": [ + { + "name": "@effect/language-service", + "transform": "@effect/language-service/transform", + "namespaceImportPackages": ["effect", "@effect/*"] + } + ] + } +} diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index c0c9940f35e..8de440b4560 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.3.46" +version = "7.3.54" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.54/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.54/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.54/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.54/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.54/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 24c96c918d7..3569325edd5 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.46", + "version": "7.3.54", "name": "@opencode-ai/http-recorder", "type": "module", "license": "MIT", @@ -22,5 +22,6 @@ "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:" - } + }, + "peerDependencies": {} } diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 999c2cfa2f7..4cffa62ffc2 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.3.46", + "version": "7.3.54", "private": true, "type": "module", "scripts": { @@ -14,6 +14,7 @@ "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", "@lottiefiles/dotlottie-web": "0.74.0", + "@pierre/diffs": "catalog:", "@solidjs/router": "catalog:", "ghostty-web": "0.4.0", "solid-js": "catalog:", diff --git a/packages/kilo-console/src/client.ts b/packages/kilo-console/src/client.ts index 076b5f4a883..b1e74cadc10 100644 --- a/packages/kilo-console/src/client.ts +++ b/packages/kilo-console/src/client.ts @@ -11,6 +11,7 @@ import type { GlobalHealthResponse, GlobalEvent, KiloEmbeddingModelCatalog, + KiloProfileResponse, LspStatusResponse, McpStatusResponse, Pty as PtyInfo, @@ -42,6 +43,8 @@ export type Query = { export type ProjectQuery = Pick +export type KiloProfileData = KiloProfileResponse + export type ProjectItem = KiloProject export type RecentProjectItem = ProjectItem & { sessions: number @@ -210,6 +213,11 @@ function model(input: unknown) { return { provider: input.slice(0, index), model: input.slice(index + 1) } } +function directory(input: ProjectQuery) { + const dir = value(input.dir) + return dir ? { directory: dir } : undefined +} + function title(input: string) { return input .split("_") @@ -407,6 +415,42 @@ export async function loadEmbeddingModels(input: Query): Promise { + const sdk = client(input) + const result = await sdk.kilo.profile(directory(input)) + return demand("Kilo profile", result) +} + +export async function setKiloOrganization(input: ProjectQuery, organizationId: string | null) { + const sdk = client(input) + const result = await sdk.kilo.organization.set({ ...directory(input), organizationId }) + demand("Switch Kilo account", result) + await sdk.global.dispose() +} + +export async function logoutKilo(input: ProjectQuery) { + const sdk = client(input) + const result = await sdk.auth.remove({ providerID: "kilo" }) + demand("Log out of Kilo", result) + await sdk.global.dispose() +} + +export async function startKiloLogin(input: ProjectQuery): Promise { + const sdk = client(input) + const result = await sdk.provider.oauth.authorize({ ...directory(input), providerID: "kilo", method: 0 }) + return demand("Start Kilo login", result) +} + +export async function completeKiloLogin(input: ProjectQuery, signal?: AbortSignal) { + const sdk = client(input) + const result = await sdk.provider.oauth.callback( + { ...directory(input), providerID: "kilo", method: 0 }, + signal ? { signal } : undefined, + ) + demand("Complete Kilo login", result) + await sdk.global.dispose() +} + export async function loadProjects(input: ProjectQuery): Promise { const sdk = client(input) const dir = value(input.dir) diff --git a/packages/kilo-console/src/index.tsx b/packages/kilo-console/src/index.tsx index 786f9c9cadd..13036c76ffb 100644 --- a/packages/kilo-console/src/index.tsx +++ b/packages/kilo-console/src/index.tsx @@ -6,6 +6,7 @@ import "./styles.css" import { ProjectConsoleRoute } from "./routes/projects/ProjectConsoleRoute" import { ProjectsRoute } from "./routes/projects/ProjectsRoute" import { ProfileRoute } from "./routes/profile/ProfileRoute" +import { LoginRoute } from "./routes/profile/LoginRoute" import { ConfigLayout } from "./layouts/ConfigLayout" import { configSections } from "./routes/config/sections" @@ -27,6 +28,7 @@ render( {routes()} + {routes()} diff --git a/packages/kilo-console/src/routes/config/AgentsRoute.tsx b/packages/kilo-console/src/routes/config/AgentsRoute.tsx index 36d740df0df..25299e7b105 100644 --- a/packages/kilo-console/src/routes/config/AgentsRoute.tsx +++ b/packages/kilo-console/src/routes/config/AgentsRoute.tsx @@ -265,7 +265,10 @@ export function AgentBuilderRoute() { onInput={(event) => state.setDesc(event.currentTarget.value)} /> - + diff --git a/packages/kilo-console/src/routes/config/ModelsRoute.tsx b/packages/kilo-console/src/routes/config/ModelsRoute.tsx index d87e93e914c..459062d7c55 100644 --- a/packages/kilo-console/src/routes/config/ModelsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ModelsRoute.tsx @@ -40,6 +40,7 @@ function chips(model: Model) { const list: string[] = [] if (model.capabilities.toolcall) list.push("toolcall") if (model.capabilities.attachment) list.push("attachment") + if (model.capabilities.temperature) list.push("temperature") if (model.capabilities.input.image) list.push("vision") if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio") return list @@ -87,6 +88,7 @@ function Stat(props: { label: string; value: string; sub?: string; mono?: boolea const caps = [ { key: "toolcall", label: "toolcall" }, { key: "attachment", label: "attachment" }, + { key: "temperature", label: "temperature" }, { key: "input:image", label: "vision" }, { key: "input:audio", label: "audio" }, ] satisfies { key: Capability; label: string }[] @@ -419,6 +421,21 @@ export function ModelsAvailableRoute() { {fmtContext(state.top())} + +
+ Privacy + +
+
Capabilities
@@ -476,6 +493,7 @@ export function ModelsAvailableRoute() { +
diff --git a/packages/kilo-console/src/routes/config/state/models.test.ts b/packages/kilo-console/src/routes/config/state/models.test.ts new file mode 100644 index 00000000000..423c11305ea --- /dev/null +++ b/packages/kilo-console/src/routes/config/state/models.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { hasGateway, visible } from "./privacy" + +describe("model privacy filter", () => { + test("detects when Kilo Gateway models are present", () => { + expect(hasGateway([{ id: "kilo" }, { id: "openai" }])).toBe(true) + expect(hasGateway([{ id: "openai" }])).toBe(false) + }) + + test("shows every model when disabled", () => { + expect(visible({ id: "kilo" }, { mayTrainOnYourPrompts: true }, false)).toBe(true) + }) + + test("hides only Kilo Gateway models explicitly marked for prompt training", () => { + expect(visible({ id: "kilo" }, { mayTrainOnYourPrompts: true }, true)).toBe(false) + expect(visible({ id: "kilo" }, { mayTrainOnYourPrompts: false }, true)).toBe(true) + expect(visible({ id: "kilo" }, {}, true)).toBe(true) + expect(visible({ id: "openai" }, { mayTrainOnYourPrompts: true }, true)).toBe(true) + }) +}) diff --git a/packages/kilo-console/src/routes/config/state/models.ts b/packages/kilo-console/src/routes/config/state/models.ts index 715f5826cb2..9a1b5fdc8f1 100644 --- a/packages/kilo-console/src/routes/config/state/models.ts +++ b/packages/kilo-console/src/routes/config/state/models.ts @@ -2,6 +2,7 @@ import { createMemo, createSignal } from "solid-js" import type { Model, Provider } from "@kilocode/sdk/v2/client" import { saveModelState, type ModelRef } from "../../../client" import { useConfig } from "../../../context/config" +import { hasGateway, visible } from "./privacy" export const capabilities = [ "toolcall", @@ -60,6 +61,7 @@ export function useModelSettings() { const [low, setLow] = createSignal(0) const [high, setHigh] = createSignal(0) const [reason, setReason] = createSignal("all") + const [privacy, setPrivacy] = createSignal(false) const [caps, setCaps] = createSignal([]) const [starred, setStarred] = createSignal(false) const [mode, setMode] = createSignal<"closed" | "select">("closed") @@ -86,6 +88,7 @@ export function useModelSettings() { })), ) }) + const gateway = createMemo(() => hasGateway(providers())) const max = createMemo(() => { const values = all().map((item) => item.model.limit.context) @@ -108,6 +111,7 @@ export function useModelSettings() { .filter((item) => (upper() !== undefined ? item.model.limit.context <= upper()! : true)) .filter((item) => (reason() === "reasoning" ? item.model.capabilities.reasoning : true)) .filter((item) => (reason() === "standard" ? !item.model.capabilities.reasoning : true)) + .filter((item) => visible(item.provider, item.model, privacy())) .filter((item) => (starred() ? fav(item) : true)) .filter((item) => caps().every((cap) => has(item.model, cap))) .filter((item) => { @@ -221,6 +225,8 @@ export function useModelSettings() { setMax, reason, setReason, + privacy, + setPrivacy, caps, setCaps, starred, @@ -228,6 +234,7 @@ export function useModelSettings() { fav, favorite, max, + gateway, providers, capabilities, toggle, diff --git a/packages/kilo-console/src/routes/config/state/privacy.ts b/packages/kilo-console/src/routes/config/state/privacy.ts new file mode 100644 index 00000000000..82bf18ecc09 --- /dev/null +++ b/packages/kilo-console/src/routes/config/state/privacy.ts @@ -0,0 +1,13 @@ +import type { Model, Provider } from "@kilocode/sdk/v2/client" + +export function hasGateway(providers: Pick[]) { + return providers.some((provider) => provider.id === "kilo") +} + +export function visible( + provider: Pick, + model: Pick, + privacy: boolean, +) { + return !privacy || provider.id !== "kilo" || model.mayTrainOnYourPrompts !== true +} diff --git a/packages/kilo-console/src/routes/profile/LoginRoute.tsx b/packages/kilo-console/src/routes/profile/LoginRoute.tsx new file mode 100644 index 00000000000..3fde3f65674 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/LoginRoute.tsx @@ -0,0 +1,244 @@ +import { useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { completeKiloLogin, loadKiloProfile, startKiloLogin, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { markDisconnected, page, parseDeviceCode, safeReturn } from "./profile-utils" +import { useProfileServer } from "./server" + +type Status = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled" + +type State = { + status: Status + code?: string + url?: string + expiresIn?: number + error?: string +} + +function time(input: number) { + const min = Math.floor(input / 60) + const sec = input % 60 + return `${min}:${sec.toString().padStart(2, "0")}` +} + +export function LoginRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [auth, setAuth] = createSignal({ status: "idle" }) + const [left, setLeft] = createSignal(900) + const [attempt, setAttempt] = createSignal(0) + const [active, setActive] = createSignal() + const [copied, setCopied] = createSignal("") + const ret = () => safeReturn(params().get("return")) + const profile = () => page(params(), "/profile") + + function abort() { + active()?.abort() + setActive(undefined) + } + + function done() { + window.setTimeout(() => nav(ret(), { replace: true }), 650) + } + + function start(input: ProjectQuery | undefined = server.query()) { + if (!input) return + abort() + const ctl = new AbortController() + const rev = attempt() + 1 + setAttempt(rev) + setActive(ctl) + setAuth({ status: "initiating" }) + void startKiloLogin(input) + .then((info) => { + if (attempt() !== rev) return false + setAuth({ + status: "pending", + code: parseDeviceCode(info.instructions), + url: info.url, + expiresIn: 900, + }) + return completeKiloLogin(input, ctl.signal).then(() => true) + }) + .then((ok) => { + if (!ok || attempt() !== rev) return + setAuth({ status: "success" }) + void loadKiloProfile(input) + .then(() => markDisconnected(false)) + .catch(() => markDisconnected(false)) + .finally(done) + }) + .catch((err) => { + if (attempt() !== rev) return + setAuth({ status: "error", error: errMsg(err) }) + }) + .finally(() => { + if (active() === ctl) setActive(undefined) + }) + } + + function cancel() { + abort() + setAttempt((value) => value + 1) + setAuth({ status: "cancelled" }) + } + + function copy(label: string, value: string | undefined) { + if (!value) return + void navigator.clipboard + .writeText(value) + .then(() => { + setCopied(label) + window.setTimeout(() => { + if (copied() === label) setCopied("") + }, 1400) + }) + .catch((err) => setAuth({ status: "error", error: errMsg(err) })) + } + + function open() { + const url = auth().url + if (!url) return + window.open(url, "_blank", "noopener,noreferrer") + } + + createEffect(() => { + const current = server.query() + if (!current || auth().status !== "idle") return + start(current) + }) + + createEffect(() => { + const state = auth() + if (state.status !== "pending") return + setLeft(state.expiresIn ?? 900) + const timer = window.setInterval(() => setLeft((value) => Math.max(0, value - 1)), 1000) + onCleanup(() => window.clearInterval(timer)) + }) + + createEffect(() => { + if (auth().status === "success") server.remember() + }) + + onCleanup(() => { + abort() + setAttempt((value) => value + 1) + }) + + return ( +
+ +
+ ) +} diff --git a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx index 90d20f8a16c..71a74ac6aa3 100644 --- a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx +++ b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx @@ -1,9 +1,315 @@ +import { A, useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { loadKiloProfile, logoutKilo, setKiloOrganization, type KiloProfileData, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { + authError, + cloud, + credits, + initials, + markDisconnected, + money, + page, + personal, + usage, + wasDisconnected, +} from "./profile-utils" +import { useProfileServer } from "./server" + +type Org = NonNullable[number] +type State = { kind: "connected"; data: KiloProfileData } | { kind: "disconnected" } + +async function load(input: ProjectQuery): Promise { + if (wasDisconnected()) return { kind: "disconnected" } + try { + const data = await loadKiloProfile(input) + markDisconnected(false) + return { kind: "connected", data } + } catch (err) { + if (authError(err)) { + markDisconnected(true) + return { kind: "disconnected" } + } + throw err + } +} + +function org(data: KiloProfileData | undefined) { + if (!data?.currentOrgId) return undefined + return data.profile.organizations?.find((item) => item.id === data.currentOrgId) +} + +function account(data: KiloProfileData | undefined) { + return org(data)?.name ?? "Personal Account" +} + +function role(input: Org) { + if (input.role === "owner") return "Owner" + if (input.role === "admin") return "Admin" + if (input.role === "billing_manager") return "Billing" + return "Member" +} + export function ProfileRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [data, actions] = createResource(server.query, load) + const [saving, setSaving] = createSignal() + const [error, setError] = createSignal("") + const profile = createMemo(() => { + const state = data() + if (state?.kind === "connected") return state.data + return undefined + }) + const orgs = createMemo(() => profile()?.profile.organizations ?? []) + const active = createMemo(() => org(profile())) + const disconnected = createMemo(() => data()?.kind === "disconnected") + const scope = createMemo(() => { + if (profile()) return account(profile()) + if (data.loading) return "Loading..." + return "Not connected" + }) + const login = () => page(params(), "/kilo/login") + const overview = () => page(params(), "/profile") + const usageUrl = createMemo(() => usage(profile()?.currentOrgId)) + const creditsUrl = createMemo(() => credits(profile()?.currentOrgId)) + + createEffect(() => { + if (profile()) server.remember() + }) + + createEffect(() => { + const err = data.error + if (!err || authError(err)) return + server.recover() + }) + + function refresh() { + setError("") + void actions.refetch() + } + + function choose(id: string | null) { + const current = server.query() + if (!current) return + const next = id ?? personal + const selected = profile()?.currentOrgId ?? null + if (selected === id) return + setSaving(next) + setError("") + void setKiloOrganization(current, id) + .then(() => actions.refetch()) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + + function logout() { + const current = server.query() + if (!current) return + setSaving("logout") + setError("") + void logoutKilo(current) + .then(() => { + markDisconnected(true) + actions.mutate({ kind: "disconnected" }) + nav(overview(), { replace: true }) + }) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + return ( -
-

Profile

-

Profile Settings

-

Profile identity, account preferences, and workspace defaults will live here.

+
+ + + +
+
+ +
+
+

Kilo Account

+

Your Profile

+

Manage your Kilo identity, account context, credits, and billing shortcuts.

+
+
+ + + Open Dashboard + +
+
+
+ + + + + + + + + + + + +
+

Kilo Account

+

Connect your Kilo account

+

Sign in to view your credits, organizations, and account details in Kilo Console.

+
+ + Connect + +
+
+ + + + Kilo server not found + Start Kilo Console from a running Kilo server or pass a server URL with ?server=. + + + + + + Profile request failed + {errMsg(data.error)} + + + + + + Account update failed + {error()} + + + + + {(info) => ( +
+ + + + Remaining Credits + {money(info().balance?.balance)} +

+ Credits shown for {account(info())}. Switch accounts below to see organization balances when + available. +

+
+ + +
+
+ Organizations +

Your Accounts

+
+ {orgs().length === 1 ? "1 org" : `${orgs().length} orgs`} +
+
+ + + {(item) => ( + + )} + +
+
+
+ )} +
+
+
) } diff --git a/packages/kilo-console/src/routes/profile/profile-utils.test.ts b/packages/kilo-console/src/routes/profile/profile-utils.test.ts new file mode 100644 index 00000000000..95ed86a0e9c --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { authError, credits, initials, money, page, parseDeviceCode, safeReturn, usage } from "./profile-utils" + +test("parses device auth codes from instructions", () => { + expect(parseDeviceCode("Open https://app.kilo.ai/device-auth and enter code: ABCD-2345")).toBe("ABCD-2345") + expect(parseDeviceCode("Use ABCD-2345 to continue")).toBe("ABCD-2345") + expect(parseDeviceCode(undefined)).toBeUndefined() +}) + +test("detects Kilo auth failures", () => { + expect(authError(new Error("Kilo profile: Unauthorized"))).toBe(true) + expect(authError({ status: 401 })).toBe(true) + expect(authError(new Error("Kilo profile: temporary network failure"))).toBe(false) +}) + +test("formats account display helpers", () => { + expect(money(12.5)).toBe("$12.50") + expect(money(null)).toBe("Unknown") + expect(initials("Jane Developer", "jane@example.com")).toBe("JD") + expect(initials("", "solo@example.com")).toBe("SE") +}) + +test("keeps login returns internal", () => { + expect(safeReturn("/profile?server=http%3A%2F%2F127.0.0.1%3A4097")).toBe( + "/profile?server=http%3A%2F%2F127.0.0.1%3A4097", + ) + expect(safeReturn("https://app.kilo.ai/profile")).toBe("/profile") + expect(safeReturn("//app.kilo.ai/profile")).toBe("/profile") +}) + +test("builds local links while preserving server", () => { + const params = new URLSearchParams({ server: "http://127.0.0.1:4097", ignored: "1" }) + expect(page(params, "/kilo/login", { return: "/profile" })).toBe( + "/kilo/login?server=http%3A%2F%2F127.0.0.1%3A4097&return=%2Fprofile", + ) +}) + +test("builds account-aware cloud links", () => { + const id = "9d4b144c-0a2b-477b-973d-24fa02bebf13" + expect(usage(null)).toBe("https://app.kilo.ai/usage") + expect(usage(id)).toBe(`https://app.kilo.ai/organizations/${id}/usage-details`) + expect(credits(undefined)).toBe("https://app.kilo.ai/profile") + expect(credits(id)).toBe(`https://app.kilo.ai/organizations/${id}`) +}) diff --git a/packages/kilo-console/src/routes/profile/profile-utils.ts b/packages/kilo-console/src/routes/profile/profile-utils.ts new file mode 100644 index 00000000000..b61e6e0347f --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.ts @@ -0,0 +1,80 @@ +export const personal = "personal" + +let marked = false + +export function markDisconnected(input: boolean) { + marked = input +} + +export function wasDisconnected() { + return marked +} + +function text(input: unknown) { + if (input instanceof Error) return input.message + if (typeof input === "string") return input + if (input === undefined || input === null) return "" + return JSON.stringify(input) +} + +export function parseDeviceCode(input: string | undefined) { + if (!input) return undefined + const code = input.match(/code:\s*([A-Z0-9-]+)/i)?.[1] + if (code) return code.toUpperCase() + return input.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i)?.[1]?.toUpperCase() +} + +export function authError(input: unknown) { + const value = text(input).toLowerCase() + return value.includes("unauthorized") || value.includes("invalid token") || value.includes('status":401') +} + +export function money(input: number | null | undefined) { + if (typeof input !== "number" || !Number.isFinite(input)) return "Unknown" + return `$${input.toFixed(2)}` +} + +export function initials(name: string | undefined, email: string) { + const parts = (name?.trim() || email) + .split(/[\s._@-]+/) + .filter(Boolean) + .slice(0, 2) + const value = parts.map((part) => part[0] ?? "").join("") + return value.toUpperCase() || "KG" +} + +export function safeReturn(input: string | null | undefined) { + if (!input || !input.startsWith("/") || input.startsWith("//")) return "/profile" + try { + const url = new URL(input, "http://localhost") + if (url.origin !== "http://localhost") return "/profile" + return `${url.pathname}${url.search}${url.hash}` + } catch { + return "/profile" + } +} + +export function page(params: URLSearchParams, path: string, extra?: Record) { + const next = new URLSearchParams() + const server = params.get("server") + if (server) next.set("server", server) + for (const [key, value] of Object.entries(extra ?? {})) { + if (value) next.set(key, value) + } + const query = next.toString() + return `${path}${query ? `?${query}` : ""}` +} + +export function cloud(path = "/profile") { + return `https://app.kilo.ai${path}` +} + +export function usage(id: string | null | undefined) { + if (!id) return cloud("/usage") + return cloud(`/organizations/${encodeURIComponent(id)}/usage-details`) +} + +export function credits(id: string | null | undefined) { + if (!id) return cloud("/profile") + return cloud(`/organizations/${encodeURIComponent(id)}`) +} diff --git a/packages/kilo-console/src/routes/profile/server.ts b/packages/kilo-console/src/routes/profile/server.ts new file mode 100644 index 00000000000..82c748a6185 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/server.ts @@ -0,0 +1,71 @@ +import { createEffect, createMemo, createSignal } from "solid-js" +import { + discover, + forgetCached, + loadCached, + resolveServer, + saveCached, + type ProjectQuery, +} from "../../client" +import { clean } from "../../shared/utils" + +const ports = new Set(["3017", "3018"]) + +function shouldDiscover(input: URLSearchParams) { + if (input.get("server")) return false + return ports.has(window.location.port) +} + +function base(input: URLSearchParams) { + const param = input.get("server") + if (param) return param + const cached = shouldDiscover(input) ? loadCached() : "" + if (cached) return cached + if (shouldDiscover(input)) return "" + return window.location.origin +} + +export function useProfileServer(params: () => URLSearchParams) { + const [url, setUrl] = createSignal(base(params())) + const discoverable = () => shouldDiscover(params()) + const query = createMemo(() => { + const target = clean(url()) || base(params()) + if (!target) return undefined + return { url: target, dir: "" } + }) + + createEffect(() => { + const next = params().get("server") + if (next && next !== url()) setUrl(next) + }) + + createEffect(() => { + if (!discoverable()) return + void resolveServer().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + }) + + function remember() { + const current = query() + if (!current || !discoverable()) return + saveCached(current.url) + } + + function recover() { + if (!discoverable()) return + const cached = loadCached() + if (!cached || cached !== url()) return + forgetCached() + setUrl("") + void discover().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + } + + return { query, discoverable, remember, recover } +} diff --git a/packages/kilo-console/src/shared/navigation.test.ts b/packages/kilo-console/src/shared/navigation.test.ts index 01f36705c1a..53f6682ceac 100644 --- a/packages/kilo-console/src/shared/navigation.test.ts +++ b/packages/kilo-console/src/shared/navigation.test.ts @@ -11,6 +11,7 @@ test("classifies routes after stripping the console base", () => { expect(path("/console/projects/demo/settings/agents", "/console")).toBe("/project") expect(path("/console/settings/agents", "/console")).toBe("/settings") expect(path("/console/profile", "/console")).toBe("/profile") + expect(path("/console/kilo/login", "/console")).toBe("/profile") }) test("builds settings roots without preserving the console base", () => { diff --git a/packages/kilo-console/src/shared/navigation.ts b/packages/kilo-console/src/shared/navigation.ts index 3d1476eebe5..195f491a0ae 100644 --- a/packages/kilo-console/src/shared/navigation.ts +++ b/packages/kilo-console/src/shared/navigation.ts @@ -21,7 +21,7 @@ export function settings(input: string, prefix = base()) { export function path(input: string, prefix = base()): Path { const route = strip(input, prefix) - if (route === "/profile") return "/profile" + if (route === "/profile" || route.startsWith("/kilo/login")) return "/profile" if (route.startsWith("/settings") || route.startsWith("/config")) return "/settings" if (route.startsWith("/projects/")) return "/project" return "/projects" diff --git a/packages/kilo-console/src/styles.css b/packages/kilo-console/src/styles.css index 5ab4a61da3f..bad09eb3ec7 100644 --- a/packages/kilo-console/src/styles.css +++ b/packages/kilo-console/src/styles.css @@ -14,6 +14,7 @@ @import "./styles/agents-tools.css"; @import "./styles/projects.css"; @import "./styles/project-console.css"; +@import "./styles/profile.css"; @import "./styles/servers.css"; @import "./styles/sources.css"; @import "./styles/responsive.css"; diff --git a/packages/kilo-console/src/styles/models.css b/packages/kilo-console/src/styles/models.css index 626f43786a4..d69baf28566 100644 --- a/packages/kilo-console/src/styles/models.css +++ b/packages/kilo-console/src/styles/models.css @@ -40,6 +40,7 @@ .kilo-console .models-filter-field input, .kilo-console .models-select summary, .kilo-console .models-favorite-filter, +.kilo-console .models-privacy-filter, .kilo-console .models-capability-toggle { height: 2rem; min-height: 2rem; @@ -156,6 +157,7 @@ } .kilo-console .models-favorite-filter, +.kilo-console .models-privacy-filter, .kilo-console .models-capability-toggle { display: inline-flex; gap: 0.375rem; @@ -172,8 +174,10 @@ } .kilo-console .models-favorite-filter:hover, +.kilo-console .models-privacy-filter:hover, .kilo-console .models-capability-toggle:hover, .kilo-console .models-favorite-filter.selected, +.kilo-console .models-privacy-filter.selected, .kilo-console .models-capability-toggle.selected { border-color: var(--primary); background: color-mix(in oklab, var(--primary) 10%, transparent); @@ -202,6 +206,7 @@ } .kilo-console .models-context-filter > span, +.kilo-console .models-privacy-filter-group > span, .kilo-console .models-capabilities-filter > span { flex: 0 0 auto; color: var(--muted-foreground); @@ -258,6 +263,7 @@ text-align: right; } +.kilo-console .models-privacy-filter-group, .kilo-console .models-capabilities-filter { flex: 0 0 auto; display: flex; @@ -272,6 +278,10 @@ flex-wrap: wrap; } +.kilo-console .models-capabilities-list button { + text-transform: capitalize; +} + .kilo-console .explore-models { grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr)); gap: 0.625rem; diff --git a/packages/kilo-console/src/styles/profile.css b/packages/kilo-console/src/styles/profile.css new file mode 100644 index 00000000000..c5630cfa28e --- /dev/null +++ b/packages/kilo-console/src/styles/profile.css @@ -0,0 +1,480 @@ +.kilo-console .profile-page, +.kilo-console .profile-login-page { + display: grid; + gap: 1rem; + max-width: 64rem; + margin: 0 auto; +} + +.kilo-console .profile-login-page { + max-width: 34rem; +} + +.kilo-console .profile-shell.disconnected { + grid-template-columns: minmax(0, 1fr); +} + +.kilo-console .profile-header, +.kilo-console .profile-login-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: start; + padding-bottom: 0.5rem; +} + +.kilo-console .profile-login-header { + grid-template-columns: 1fr; + text-align: center; +} + +.kilo-console .profile-header h1, +.kilo-console .profile-login-header h1 { + margin: 0; + color: var(--foreground); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-header p:not(.eyebrow), +.kilo-console .profile-login-header p:not(.eyebrow) { + max-width: 42rem; + margin: 0.625rem 0 0; + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.45; +} + +.kilo-console .profile-actions, +.kilo-console .profile-card-actions, +.kilo-console .profile-login-actions { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; +} + +.kilo-console .profile-primary-link { + text-decoration: none; +} + +.kilo-console .profile-connect-card { + display: grid; + min-height: 18rem; + place-items: center; + align-content: center; + gap: 1.25rem; + text-align: center; +} + +.kilo-console .profile-connect-mark { + display: grid; + width: 3.5rem; + height: 3.5rem; + place-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 32%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.kilo-console .profile-connect-copy { + display: grid; + max-width: 28rem; + gap: 0.625rem; +} + +.kilo-console .profile-connect-copy h1, +.kilo-console .profile-connect-copy p { + margin: 0; +} + +.kilo-console .profile-connect-copy h1 { + color: var(--foreground); + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-connect-copy p:not(.eyebrow) { + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.5; +} + +.kilo-console .profile-link-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-size: 0.75rem; + font-weight: 500; + line-height: 1.45; + padding: 0.25rem 0.625rem; + text-decoration: none; + transition: + background 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.kilo-console .profile-link-button:hover, +.kilo-console .profile-link-button:focus-visible { + border-color: var(--ring); + background: var(--muted); + color: var(--foreground); + outline: 0; +} + +.kilo-console .profile-banner { + margin-bottom: 0; +} + +.kilo-console .profile-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.kilo-console .profile-card { + gap: 0.875rem; +} + +.kilo-console .profile-wide-card { + grid-column: 1 / -1; +} + +.kilo-console .profile-account-card, +.kilo-console .profile-balance-card { + min-height: 10rem; +} + +.kilo-console .profile-account-head { + display: flex; + gap: 0.875rem; + align-items: center; + min-width: 0; +} + +.kilo-console .profile-avatar, +.kilo-console .profile-org-icon { + display: grid; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--border); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 28%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-weight: 700; + letter-spacing: 0.06em; +} + +.kilo-console .profile-avatar { + width: 3.25rem; + height: 3.25rem; + border-radius: var(--radius-lg); + font-size: 1rem; +} + +.kilo-console .profile-org-icon { + width: 2rem; + height: 2rem; + border-radius: var(--radius-sm); + font-size: 0.6875rem; +} + +.kilo-console .profile-account-text { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-account-text strong, +.kilo-console .profile-card-head h2, +.kilo-console .profile-login-state strong, +.kilo-console .profile-login-step strong { + margin: 0; + color: var(--foreground); + font-size: 0.9375rem; + font-weight: 600; + line-height: 1.25; +} + +.kilo-console .profile-account-text span, +.kilo-console .profile-login-state p, +.kilo-console .profile-login-step > span, +.kilo-console .profile-login-waiting { + color: var(--muted-foreground); + font-size: 0.75rem; + line-height: 1.45; +} + +.kilo-console .profile-account-text span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-meta-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: color-mix(in oklab, var(--muted) 35%, transparent); + padding: 0.75rem; +} + +.kilo-console .profile-meta-row span, +.kilo-console .profile-card-label { + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.kilo-console .profile-meta-row strong { + color: var(--foreground); + font-size: 0.75rem; + font-weight: 600; +} + +.kilo-console .profile-balance-card strong { + color: var(--foreground); + font-size: clamp(2rem, 6vw, 3.25rem); + font-weight: 700; + letter-spacing: -0.05em; + line-height: 0.95; +} + +.kilo-console .profile-balance-card p, +.kilo-console .profile-login-state p { + margin: 0; +} + +.kilo-console .profile-card-head { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; +} + +.kilo-console .profile-pill { + display: inline-flex; + align-items: center; + min-height: 1.5rem; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + padding: 0 0.625rem; + text-decoration: none; + text-transform: uppercase; +} + +.kilo-console .profile-org-list { + display: grid; + gap: 0.5rem; +} + +.kilo-console .profile-org-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; + min-height: 3.5rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--card); + color: var(--foreground); + cursor: pointer; + font: inherit; + padding: 0.75rem; + text-align: left; + transition: + background 120ms ease, + border-color 120ms ease; +} + +.kilo-console .profile-org-row:hover, +.kilo-console .profile-org-row:focus-visible, +.kilo-console .profile-org-row.active { + border-color: var(--ring); + background: color-mix(in oklab, var(--muted) 45%, transparent); + outline: 0; +} + +.kilo-console .profile-org-row:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +.kilo-console .profile-org-body { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-org-body strong { + overflow: hidden; + color: var(--foreground); + font-size: 0.8125rem; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-org-body span, +.kilo-console .profile-org-state { + color: var(--muted-foreground); + font-size: 0.6875rem; +} + +.kilo-console .profile-org-state { + font-weight: 600; +} + +.kilo-console .profile-login-card { + min-height: 18rem; +} + +.kilo-console .profile-login-state, +.kilo-console .profile-login-flow, +.kilo-console .profile-login-step { + display: grid; + gap: 0.875rem; +} + +.kilo-console .profile-login-state { + place-items: center; + min-height: 14rem; + text-align: center; +} + +.kilo-console .profile-login-state.error strong { + color: var(--destructive); +} + +.kilo-console .profile-login-state.success strong { + color: var(--success, var(--primary)); +} + +.kilo-console .profile-login-step { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--muted) 25%, transparent); + padding: 1rem; +} + +.kilo-console .profile-login-url { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.5rem; + align-items: center; +} + +.kilo-console .profile-login-url span { + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-family: var(--font-family-mono, monospace); + font-size: 0.75rem; + padding: 0.5rem 0.625rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-login-code { + display: grid; + gap: 0.25rem; + place-items: center; + border: 1px solid var(--ring); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--primary) 12%, var(--input-base)); + color: var(--foreground); + cursor: pointer; + font-family: var(--font-family-mono, monospace); + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.16em; + padding: 1rem; +} + +.kilo-console .profile-login-code small { + color: var(--muted-foreground); + font-family: var(--font-family-sans, system-ui, sans-serif); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0; +} + +.kilo-console .profile-login-waiting { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.kilo-console .profile-spinner { + width: 1rem; + height: 1rem; + border: 2px solid color-mix(in oklab, var(--muted-foreground) 30%, transparent); + border-top-color: var(--primary); + border-radius: 999px; + animation: profile-spin 900ms linear infinite; +} + +@keyframes profile-spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 760px) { + .kilo-console .profile-header, + .kilo-console .profile-grid { + grid-template-columns: 1fr; + } + + .kilo-console .profile-actions, + .kilo-console .profile-card-actions, + .kilo-console .profile-login-actions { + justify-content: flex-start; + } + + .kilo-console .profile-login-url { + grid-template-columns: 1fr; + } +} + +@media (max-width: 520px) { + .kilo-console .profile-org-row { + grid-template-columns: auto minmax(0, 1fr); + } + + .kilo-console .profile-org-state { + grid-column: 2; + } + + .kilo-console .profile-login-code { + font-size: 1.5rem; + } +} diff --git a/packages/kilo-console/src/styles/responsive.css b/packages/kilo-console/src/styles/responsive.css index 0944710f546..ad03bb12438 100644 --- a/packages/kilo-console/src/styles/responsive.css +++ b/packages/kilo-console/src/styles/responsive.css @@ -76,6 +76,7 @@ } .kilo-console .models-context-filter, + .kilo-console .models-privacy-filter-group, .kilo-console .models-capabilities-filter { flex: 1 1 auto; min-width: 0; diff --git a/packages/kilo-docs/docs/getting-started/switching-from-cline.md b/packages/kilo-docs/docs/getting-started/switching-from-cline.md index 8f04bb864ca..2c7dcd6be0c 100644 --- a/packages/kilo-docs/docs/getting-started/switching-from-cline.md +++ b/packages/kilo-docs/docs/getting-started/switching-from-cline.md @@ -202,9 +202,9 @@ One-click deployments from directly within Kilo. Go from code to production with Automatically analyzes your PRs using your choice of AI model. Reviews happen the moment a PR is opened or updated, covering performance, security, style, and test coverage. -### Managed Indexing +### Codebase Indexing -Semantic search across your repositories using cloud-hosted embeddings. Kilo indexes your codebase to deliver more relevant, context-aware responses. +Semantic search across your repositories using configurable embedding providers and vector stores. Kilo indexes your codebase to deliver more relevant, context-aware responses. ### Autocomplete diff --git a/packages/kilo-docs/lib/nav/customize.ts b/packages/kilo-docs/lib/nav/customize.ts index a3bd302034b..77908c54483 100644 --- a/packages/kilo-docs/lib/nav/customize.ts +++ b/packages/kilo-docs/lib/nav/customize.ts @@ -5,6 +5,7 @@ export const CustomizeNav: NavSection[] = [ title: "Customization", links: [ { href: "/customize", children: "Overview" }, + { href: "/customize/marketplace", children: "Marketplace", platform: "new" }, { href: "/customize/custom-modes", children: "Custom Modes", diff --git a/packages/kilo-docs/lib/nav/deploy-secure.ts b/packages/kilo-docs/lib/nav/deploy-secure.ts index 7c41f137996..901eed7c003 100644 --- a/packages/kilo-docs/lib/nav/deploy-secure.ts +++ b/packages/kilo-docs/lib/nav/deploy-secure.ts @@ -6,11 +6,10 @@ export const DeploySecureNav: NavSection[] = [ links: [ { href: "/deploy-secure", children: "Overview" }, { href: "/deploy-secure/deploy", children: "Deploy" }, - { href: "/deploy-secure/managed-indexing", children: "Managed Indexing" }, ], }, { title: "Security", - links: [{ href: "/deploy-secure/security-reviews", children: "Security Reviews" }], + links: [{ href: "/deploy-secure/security-reviews", children: "Security Agent" }], }, ] diff --git a/packages/kilo-docs/mappingplan.md b/packages/kilo-docs/mappingplan.md index d94063a4f67..7131580834f 100644 --- a/packages/kilo-docs/mappingplan.md +++ b/packages/kilo-docs/mappingplan.md @@ -113,7 +113,6 @@ | New Item | Existing Page(s) | |---|---| | Deploy | `advanced-usage/deploy` | -| Managed Indexing | `advanced-usage/managed-indexing` | | Security Reviews | `contributing/architecture/security-reviews` (move out of contributing) | --- diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md index 877e4a17785..65bfbedcf2c 100644 --- a/packages/kilo-docs/markdoc/partials/cli-commands-table.md +++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md @@ -25,7 +25,7 @@ | `kilo session` | manage sessions | | `kilo remote` | enable remote connection for real-time session relay | | `kilo daemon` | manage the local kilo daemon | -| `kilo console` | open the local Kilo Console | +| `kilo console` | open or stop the local Kilo Console | | `kilo db` | database tools | | `kilo config` | configuration tools | | `kilo plugin ` | install plugin and update config | diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 6b246174f17..a631b4b5a35 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.3.46", + "version": "7.3.54", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/pages/ai-providers/openai-compatible.md b/packages/kilo-docs/pages/ai-providers/openai-compatible.md index 02af740576e..39bf874c40f 100644 --- a/packages/kilo-docs/pages/ai-providers/openai-compatible.md +++ b/packages/kilo-docs/pages/ai-providers/openai-compatible.md @@ -59,7 +59,8 @@ You'll find these settings in the Kilo Code settings panel (click the {% codicon - **Provider ID** — A unique identifier (e.g., `my-provider`). - **Display name** — A human-readable name shown in the UI. -- **Base URL** — The provider's OpenAI-compatible API endpoint (e.g., `https://api.your-provider.com/v1`). Kilo auto-fetches available models when a valid URL is entered. For Azure OpenAI GPT-5, use the native `azure` provider instead. +- **Provider API** — Select **OpenAI Compatible** for an OpenAI Chat Completions-compatible endpoint. Use **OpenAI Responses** for OpenAI and xAI models. Use **Anthropic Messages** for Anthropic and MiniMax models. +- **Base URL** — The provider's API endpoint (e.g., `https://api.your-provider.com/v1`). Kilo auto-fetches available models when a valid URL exposes an OpenAI-compatible models endpoint. For Azure OpenAI GPT-5, use the native `azure` provider instead. - **API key** — Your API key. Optional — leave empty if authentication is handled via headers. - **Models** — Add models manually or select from the auto-fetched list (see [Automatic Model Detection](#automatic-model-detection) below). - **Headers** (optional) — Custom HTTP headers as key-value pairs. @@ -91,6 +92,7 @@ You must define at least one model. Setting `name` and `limit` (context window a { "provider": { "vllm": { + "npm": "@ai-sdk/openai-compatible", "models": { "qwen35": { "name": "Qwen 3.5", @@ -119,8 +121,9 @@ Then set your default model using the `provider-id/model-id` format: **Configuration fields:** +- **`npm`** — The API protocol package. Use `@ai-sdk/openai-compatible` for OpenAI Chat Completions-compatible endpoints (the default when omitted). Other possible values include `@ai-sdk/openai` for OpenAI Responses endpoints and `@ai-sdk/anthropic` for Anthropic Messages endpoints. - **`models`** — A map of model IDs to model definitions. Each model should include a `name` and `limit` with `context` and `output` token counts. If `limit.context` or `limit.output` is omitted, it defaults to `0`, which limits context management. -- **`options.baseURL`** — The base URL of your OpenAI-compatible API endpoint. For Azure OpenAI GPT-5, configure `provider.azure` instead. +- **`options.baseURL`** — The base URL of your provider's API endpoint. For Azure OpenAI GPT-5, configure `provider.azure` instead. - **`options.apiKey`** — Your API key. Use any non-empty string (e.g., `"none"`) if the provider doesn't require authentication. You can also set the API key via an environment variable instead of putting it in the config file. Use the `env` field to specify which variable to read: diff --git a/packages/kilo-docs/pages/ai-providers/v0.md b/packages/kilo-docs/pages/ai-providers/v0.md index bd1d1b16c37..8ebd95c107d 100644 --- a/packages/kilo-docs/pages/ai-providers/v0.md +++ b/packages/kilo-docs/pages/ai-providers/v0.md @@ -34,7 +34,7 @@ Setting up v0 in Kilo Code is straightforward: {% /tab %} {% tab label="VSCode" %} -Open **Settings** (gear icon) and go to the **Providers** tab to add an OpenAI Compatible provider. Set the base URL to `https://api.v0.dev/v1` and enter your v0 API key. +Open **Settings** (gear icon) and go to the **Providers** tab to add a **Custom provider**. Select **OpenAI Compatible** under **Provider API**, set the base URL to `https://api.v0.dev/v1`, and enter your v0 API key. The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format. diff --git a/packages/kilo-docs/pages/automate/code-reviews/github.md b/packages/kilo-docs/pages/automate/code-reviews/github.md index 4268264b0d2..f543e45b0b1 100644 --- a/packages/kilo-docs/pages/automate/code-reviews/github.md +++ b/packages/kilo-docs/pages/automate/code-reviews/github.md @@ -40,7 +40,7 @@ The GitHub App requests the following permissions: - **Repository Selection** — All repositories or select specific ones - **Focus Areas** — Security, performance, bugs, style, testing, documentation - **Max Review Time** — 5 to 30 minutes - - **Custom Instructions** — Add team-specific review guidelines + - **Use REVIEW.md** — Load repository-specific review guidance, including sub-agent usage, from the base branch 4. Click **Save Configuration** ### Step 3: Open a Pull Request diff --git a/packages/kilo-docs/pages/automate/code-reviews/gitlab.md b/packages/kilo-docs/pages/automate/code-reviews/gitlab.md index 11969ace77c..cccc0541d60 100644 --- a/packages/kilo-docs/pages/automate/code-reviews/gitlab.md +++ b/packages/kilo-docs/pages/automate/code-reviews/gitlab.md @@ -37,7 +37,7 @@ Once connected, return here to configure the Review Agent. - **Repository Selection** — All repositories or select specific ones - **Focus Areas** — Security, performance, bugs, style, testing, documentation - **Max Review Time** — 5 to 30 minutes - - **Custom Instructions** — Add team-specific review guidelines + - **Use REVIEW.md** — Load repository-specific review guidance, including sub-agent usage, from the base branch 4. Click **Save Configuration** When you select repositories, Kilo **automatically creates webhooks** on each project. diff --git a/packages/kilo-docs/pages/automate/code-reviews/overview.md b/packages/kilo-docs/pages/automate/code-reviews/overview.md index fe835f71b83..71a4fe33638 100644 --- a/packages/kilo-docs/pages/automate/code-reviews/overview.md +++ b/packages/kilo-docs/pages/automate/code-reviews/overview.md @@ -14,6 +14,7 @@ Kilo's **Code Reviews** feature automatically analyzes your pull or merge reques - Automatic detection of bugs, security risks, and anti-patterns - Deep reasoning over changed files, diffs, and repo context - Customizable review strictness and focus areas +- Repository-owned review guidance through `REVIEW.md`, including sub-agent usage ## Supported Platforms @@ -45,13 +46,73 @@ Before enabling Code Reviews: 5. Choose which **repositories** should receive automatic reviews. 6. Optionally select **Focus Areas** such as security, performance, bugs, style, testing, or documentation. 7. Set a **maximum review time** (5–30 minutes). -8. Add **custom instructions** to shape how the agent reviews your code. +8. Optionally enable **Use REVIEW.md** and add a `REVIEW.md` file at the repository root to shape how the agent reviews your code. Once configured, the Review Agent runs automatically on PR/MR events. For platform-specific setup, see: - [GitHub Code Reviews](./github) - [GitLab Code Reviews](./gitlab) +## Repository Guidance with REVIEW.md + +Use `REVIEW.md` when review policy should live with the repository instead of only in the Kilo dashboard. This is the best place to document domain-specific rules, severity calibration, files to skip, verification expectations, summary style, and how Kilo should use sub-agents. + +To use it: + +1. Create `REVIEW.md` at the repository root. +2. Commit it to the base branch used by pull requests or merge requests. +3. Open Code Reviews settings in the [Kilo web app](https://app.kilo.ai/code-reviews) and enable **Use REVIEW.md**. +4. Save the configuration and run a review. + +Kilo reads `REVIEW.md` from the PR/MR base branch, not the feature branch. That prevents an unreviewed change from rewriting the review policy used to evaluate itself. If the file is disabled, missing, empty, or unreadable, Kilo falls back to built-in guidance. If it is longer than 10,000 characters, Kilo truncates it and notes that in the review summary footer. + +### Default Sub-Agent Usage + +By default, Code Reviews uses sub-agents only when they materially improve coverage. After reading the diff, the reviewer estimates changed file count and changed lines, then chooses the largest tier triggered by either signal. + +| Diff size | Default behavior | Why | +|---|---|---| +| Tiny: up to 2 files and under 100 changed lines | Use 0 sub-agents and review directly | The coordination cost is higher than the coverage benefit for very small changes. | +| Small: 3-5 files or 100-300 changed lines | Use at most 1 sub-agent for a distinct risky area | One focused second pass can help without creating duplicate or low-signal findings. | +| Medium and larger: 6+ files or more than 300 changed lines | Use the full 6 sub-agents, sharded by independent areas | Larger diffs benefit from parallel coverage across files, domains, and risk categories. | + +The reviewer does not spawn sub-agents for a single-file or straightforward typo/config change. Sub-agents are read-only and do not post comments themselves. They return findings with path, line, severity, and rationale. The main reviewer remains responsible for verifying findings, removing duplicates, checking that inline comments target valid diff lines, and posting the final comments and summary. + +#### Changing Sub-Agent Behavior + +`REVIEW.md` can replace the default sub-agent guidance. Use it to change both how many sub-agents Kilo should use and what each one should inspect. + +Good sub-agent guidance is explicit about: + +- When to use 0 sub-agents. +- When to use 1-2 targeted sub-agents. +- When to use the full 6 sub-agents. +- Which areas each sub-agent should own. +- What each sub-agent should return to the main reviewer. +- Which hard constraints still matter, such as read-only review and no direct commenting by sub-agents. + +Example `REVIEW.md` section: + +```markdown +## Sub-agent usage + +Use 0 sub-agents for docs-only, formatting-only, dependency-lockfile-only, or single-file typo changes. + +Use 1 sub-agent for focused changes under 300 changed lines when the diff touches one risky area, such as authentication, billing, database migrations, or security-sensitive parsing. + +Use 3 sub-agents when a PR spans API, data model, and UI changes: + +1. API/data reviewer: check request validation, authorization, persistence, and migration safety. +2. UI reviewer: check user-visible behavior, accessibility, empty states, and error states. +3. Test reviewer: check that tests cover the observable behavior and important edge cases. + +Use the full 6 sub-agents only for large cross-cutting changes, security-sensitive work, or changes above 800 changed lines. Split them by independent domains rather than asking every sub-agent to review the same files. + +Each sub-agent must stay read-only, must not post comments, and must return findings with path, line, severity, rationale, and confidence. The main reviewer must verify every finding before posting it. +``` + +`REVIEW.md` can change review policy and sub-agent usage, but it cannot override Kilo's hard safety constraints, read-only mode, non-interactive execution, platform API instructions, diff-line rules, duplicate-comment rules, or output formatting requirements. + ## Local Code Reviews Code Reviewer is also available locally. This is valuable for developers who want to review their code before pushing a pull request to their team publicly, or for developers who want reviews and don't need to ship a pull request to GitHub. @@ -179,6 +240,6 @@ The Review Agent is ideal for: - Reviews can run for **up to 30 minutes** depending on your setting. - The agent reviews **only the changed files**, not the entire repository. -- Some highly dynamic or domain-specific code may require additional context in custom instructions. +- Some highly dynamic or domain-specific code may require additional context in `REVIEW.md`. - The agent will only run on **selected repositories**. - During beta, review capacity may be throttled for extremely large PRs. diff --git a/packages/kilo-docs/pages/automate/mcp/overview.md b/packages/kilo-docs/pages/automate/mcp/overview.md index a37ecc442e2..139c4d691ac 100644 --- a/packages/kilo-docs/pages/automate/mcp/overview.md +++ b/packages/kilo-docs/pages/automate/mcp/overview.md @@ -7,6 +7,8 @@ description: "Overview of the Model Context Protocol" The Model Context Protocol (MCP) is a standard for extending Kilo Code's capabilities by connecting to external tools and services. MCP servers provide additional tools and resources that help Kilo Code accomplish tasks beyond its built-in capabilities, such as accessing databases, custom APIs, and specialized functionality. +You can install curated MCP servers from the Kilo Marketplace. See the [Marketplace guide](/docs/customize/marketplace) to understand project and global installs, which files change, and what to review before installing. + ## MCP Documentation This documentation is organized into several sections: diff --git a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md index 189db05657a..5d9fc1fcfde 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md @@ -11,6 +11,7 @@ Auto Model is a smart routing system that selects an underlying model for each r |---|---|---| | `kilo-auto/frontier` | Maximum capability with the best available models | Paid | | `kilo-auto/balanced` | Strong performance at a lower cost | Paid | +| `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid | | `kilo-auto/free` | The best free models available | Free | ## How It Works @@ -31,8 +32,17 @@ The underlying models behind each Auto Model tier are updated server-side as bet - **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work. - **Balanced** — Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices. +- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced. - **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time. +### How Auto Efficient routing works + +1. Kilo observes your coding session in context +2. It classifies each task by complexity +3. It routes to the benchmark-proven best model for that task automatically + +You get lean costs on routine work and stronger models when the work demands it — with no manual switching. + {% callout type="warning" title="Data handling for Auto Free" %} Auto Free may route your requests to providers that log prompts and outputs and use them to improve their services. Do not submit personal or confidential data when using Auto Free. In particular, it may route to NVIDIA's free endpoints. @@ -51,7 +61,7 @@ No need to manually switch models when changing modes. Auto Model handles routin ### Flexible Cost Control -Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Free costs nothing. +Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing. ## Requirements diff --git a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md index a8b035662c7..103043b6443 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md @@ -10,7 +10,7 @@ Kilo Code ships with a curated list of models for each provider, but you can use - Using a newly released model before it's added to the built-in catalog - Running a custom or fine-tuned model via LM Studio, Ollama, or another local provider -- Connecting to a self-hosted model behind an OpenAI-compatible API +- Connecting to a self-hosted model through a custom API endpoint - Configuring model-specific options like token limits, pricing, or reasoning settings ## Defining a Custom Model @@ -32,7 +32,8 @@ Add custom models under the `provider..models` key in your config f - **Provider ID** — A unique identifier using lowercase letters, numbers, hyphens, or underscores (e.g., `myprovider`). This becomes the `provider_id` in the `provider_id/model_id` format. - **Display name** — A human-readable name shown in the UI (e.g., `My AI Provider`). -- **Base URL** — The OpenAI-compatible API endpoint (e.g., `https://api.myprovider.com/v1`). When a valid URL is entered, Kilo automatically fetches available models from the endpoint. +- **Provider API** — The protocol used by the provider. Use **OpenAI Responses** for OpenAI and xAI models. Use **Anthropic Messages** for Anthropic and MiniMax models. **OpenAI Compatible** is the default for other OpenAI Chat Completions-compatible endpoints. +- **Base URL** — The provider's API endpoint (e.g., `https://api.myprovider.com/v1`). When a valid URL is entered, Kilo automatically fetches available models from the endpoint if it exposes an OpenAI-compatible models endpoint. - **API key** — Your provider's API key. Optional — leave empty if you manage authentication via headers. - **Models** — Add models manually by ID and display name, or select from the auto-fetched list that appears after entering a valid base URL. - **Headers** (optional) — Add custom HTTP headers as key-value pairs if your provider requires them. @@ -253,7 +254,7 @@ Connect to any provider that exposes an OpenAI-compatible API: ### Configuring model options and variants -Override options or define reasoning variants for a built-in model: +Override options or define reasoning variants for a built-in model. Variant fields are provider-specific and are merged into the request when you select that variant. ```jsonc { @@ -286,6 +287,18 @@ Override options or define reasoning variants for a built-in model: } ``` +MiniMax's OpenAI-compatible Chat Completions API supports the optional boolean `reasoning_split` field. Set it on the relevant variant to control how the API returns thinking content: + +```jsonc +"variants": { + "thinking": { + "reasoning_split": true, + }, +} +``` + +With `true`, MiniMax returns thinking separately in `reasoning_content` and `reasoning_details`. This setting changes only the response format, not whether the model thinks. Leave it unset for providers that do not support it, including MiniMax using the Anthropic Messages API. + ### Using the id field to map model names If the model key in your config differs from what the provider expects, use the `id` field: @@ -375,6 +388,7 @@ You can also set options that apply to all models from a provider: | `apiKey` | `string` | API key (supports `{env:VAR}` syntax) | | `baseURL` | `string` | Override the provider's base API URL | | `timeout` | `number \| false` | Request timeout in milliseconds. Defaults to `300000` (5 minutes); set to `false` to disable | +| `chunkTimeout` | `number` | Timeout in milliseconds between streamed response chunks. If no chunk arrives within this window, the request is aborted and retried. This catches silent provider dropouts where the TCP connection stays open but SSE streaming stops. Recommended: `15000`–`30000` (15–30 seconds) for providers with unreliable streaming. | ## Filtering Available Models diff --git a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md index 710f38bfee8..b187da64dc7 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md @@ -60,7 +60,7 @@ While the specifics change constantly, some principles stay consistent: ## Free and Budget Model Picks -You don't need a paid API key to use Kilo Code productively. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. +You don't need a paid API key to use Kilo Code productively. For the lowest cost on paid work, [Auto Efficient](/docs/code-with-ai/agents/auto-model#tiers) (`kilo-auto/efficient`) routes each request to the cheapest model proven accurate enough for that task. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. If you prefer to pick models yourself, type `free` in the model picker to filter by free models, or browse the full list at [kilo.ai/models](https://kilo.ai/models). diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index 6dc7b7467c3..55bb776f44a 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -186,6 +186,8 @@ Options: --port port for the local server (defaults to random port if no value provided) [number] --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string] --thinking show thinking blocks [boolean] + --replay replay visible session history on interactive resume [boolean] [default: false] + --replay-limit cap visible interactive replay to the newest N messages [number] -i, --interactive run in direct interactive split-footer mode [boolean] [default: false] --dangerously-skip-permissions auto-approve permissions that are not explicitly denied (dangerous!) [boolean] [default: false] --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false] @@ -889,14 +891,22 @@ Options: manage the local kilo daemon Commands: + kilo daemon start the local kilo daemon [default] kilo daemon start start the local kilo daemon kilo daemon status show local kilo daemon status kilo daemon stop stop the local kilo daemon kilo daemon restart restart the local kilo daemon Options: - --help Show help [boolean] - --version Show version number [boolean] + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + --json print daemon details as JSON [boolean] + -f, --foreground keep the command active until interrupted [boolean] ``` ### kilo daemon start @@ -905,14 +915,15 @@ Options: start the local kilo daemon Options: - --help Show help [boolean] - --version Show version number [boolean] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] - --cors additional domains to allow for CORS [array] [default: []] - --json print daemon details as JSON [boolean] + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + --json print daemon details as JSON [boolean] + -f, --foreground keep the command active until interrupted [boolean] ``` ### kilo daemon status @@ -943,29 +954,46 @@ Options: restart the local kilo daemon Options: - --help Show help [boolean] - --version Show version number [boolean] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] - --cors additional domains to allow for CORS [array] [default: []] - --json print daemon details as JSON [boolean] + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + --json print daemon details as JSON [boolean] + -f, --foreground keep the command active until interrupted [boolean] ``` ## kilo console ``` -open the local Kilo Console +open or stop the local Kilo Console + +Commands: + kilo console open the local Kilo Console [default] + kilo console stop stop the local kilo daemon Options: - --help Show help [boolean] - --version Show version number [boolean] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] - --cors additional domains to allow for CORS [array] [default: []] + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + -f, --foreground keep the command active until interrupted [boolean] +``` + +### kilo console stop + +``` +stop the local kilo daemon + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --json print daemon details as JSON [boolean] ``` ## kilo db diff --git a/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md b/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md index 5bba0acd3f9..7ae258de9f1 100644 --- a/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md +++ b/packages/kilo-docs/pages/collaborate/adoption-dashboard/for-team-leads.md @@ -71,7 +71,7 @@ Low Depth indicates that developers may be trying AI but not trusting or shippin **Actions:** -1. Enable [Managed Indexing](/docs/deploy-secure/managed-indexing) to improve context quality +1. Enable [Codebase Indexing](/docs/customize/context/codebase-indexing) to improve context quality 2. Review whether suggestions are relevant to your codebase 3. Introduce chained workflows to increase multi-stage usage @@ -119,7 +119,7 @@ Use the score tiers as milestones: **For Depth:** - "Chain Challenge" — Complete one feature using plan → build → review -- Managed Indexing rollout — Enable better context for the whole team +- Codebase Indexing rollout - Enable better context for the whole team - Deploy previews — Validate AI output before merging **For Coverage:** @@ -170,7 +170,7 @@ The AI Adoption Score is designed to be quotable: > > **Key Actions Taken:** > -> - Enabled Managed Indexing for better AI context +> - Enabled Codebase Indexing for better AI context > - Introduced Code Reviews for all PRs > - Onboarded 3 inactive team members > diff --git a/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md b/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md index 126a8adc156..949329076f5 100644 --- a/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md +++ b/packages/kilo-docs/pages/collaborate/adoption-dashboard/improving-your-score.md @@ -73,7 +73,7 @@ Linking coding → review → deploy actions significantly boosts your Depth sco If acceptance rates are low, the issue is often context. The AI is making suggestions without understanding your codebase. -**Action:** Enable [Managed Indexing](/docs/deploy-secure/managed-indexing) to give the model vector-backed search across your repository. +**Action:** Enable [Codebase Indexing](/docs/customize/context/codebase-indexing) to give the model vector-backed search across your repository. Better context leads to: @@ -171,7 +171,7 @@ Other ways to spread usage: 1. Identify your most active users and learn what they're doing 2. Introduce Code Reviews to spread usage -3. Enable Managed Indexing for better context +3. Enable Codebase Indexing for better context 4. Set a monthly score goal (e.g., "reach 55 by next month") ### If You're at 51–75 (Growing Adoption) diff --git a/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md b/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md index 5cb25aa0895..02963d704fd 100644 --- a/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md +++ b/packages/kilo-docs/pages/contributing/architecture/cli-runtime.md @@ -177,7 +177,7 @@ Remote HTTP proxy responses can include sync fence metadata. Router waits for ma ## Daemon lifecycle -`kilo daemon start|status|stop|restart` manage detached local `kilo serve` child. `kilo console` calls same start path, so it reuses healthy daemon instead of spawning second process. +`kilo daemon start|status|stop|restart` manage a detached local `kilo serve` child, with bare `kilo daemon` equivalent to `kilo daemon start`. `kilo console` calls the same start path, so it reuses a healthy daemon instead of spawning a second process, while `kilo console stop` aliases `kilo daemon stop`. | Area | Behavior | |---|---| @@ -185,6 +185,7 @@ Remote HTTP proxy responses can include sync fence metadata. Router waits for ma | Log file | `${Global.Path.log}/daemon.log`, created with mode `0600` | | Port allocation | For `--port 0`, scans `4097..4116` and chooses available port | | Child process | Detached `kilo serve --hostname --port ` process | +| Foreground mode | `--foreground` / `-f` keeps the invoking command attached; SIGINT, SIGTERM, or SIGHUP stops only the daemon identity it started or reused | | Health | Probes authenticated `/global/health` with 2 second timeout | | Reuse | Reuses daemon only when process is alive, health succeeds, and installed version matches | | Cleanup | Terminates stale process when present, clears stale state, then starts replacement | diff --git a/packages/kilo-docs/pages/customize/marketplace.md b/packages/kilo-docs/pages/customize/marketplace.md new file mode 100644 index 00000000000..a8304ec70fd --- /dev/null +++ b/packages/kilo-docs/pages/customize/marketplace.md @@ -0,0 +1,70 @@ +--- +title: "Marketplace" +description: "Install agents, skills, and MCP servers from the Kilo Marketplace" +--- + +# Marketplace + +The Kilo Marketplace provides reusable extensions for Kilo. Open **Marketplace** from the Kilo sidebar to browse, install, and remove items. + +Marketplace items are configuration and instruction files, not VS Code extensions. Installing an item adds files to either the current project or your user configuration. Kilo then discovers those files through its normal configuration system. + +## What you can install + +| Type | What it adds | What happens after installation | +|---|---|---| +| **Agent** | A reusable role with its own prompt, behavior, and permissions. | The agent becomes available in Kilo's agent selector. | +| **Skill** | Task-specific instructions and resources that Kilo can load when relevant. | Kilo can discover and load the skill during a session. | +| **MCP server** | Tools supplied by an external service or a program running on your machine. | Kilo starts or connects to the configured server when it loads the MCP configuration. | + +MCP stands for **Model Context Protocol**, a standard that lets AI applications use external tools. For example, an MCP server might let Kilo query a database, work with GitHub, or interact with a browser. See [What is MCP?](/docs/automate/mcp/what-is-mcp) for a fuller explanation. + +## Project or global + +Every installation has a scope: + +| Scope | Availability | Use it when | +|---|---|---| +| **Project** | Only the current project. Files are stored under the project's `.kilo/` directory. | The whole team should use the item, or it is specific to this repository. | +| **Global** | Every project you open on this machine. Files are stored in your user configuration. | The item is part of your personal workflow across repositories. | + +Project files can be committed to version control and shared with teammates. Global files remain on your machine and do not travel with a repository. + +When the same configuration exists at both scopes, project configuration takes precedence over global configuration. Learn more about [Kilo's configuration files and precedence](/docs/getting-started/settings#config-file-precedence). + +## Files changed by installation + +The install dialog shows the destination before it changes anything. + +| Type | Project destination | Global destination | +|---|---|---| +| Agent | `.kilo/agents/.md` | `~/.config/kilo/agents/.md` | +| Skill | `.kilo/skills//` | `~/.kilo/skills//` | +| MCP server | `.kilo/kilo.json` | `~/.config/kilo/kilo.json` | + +Installing an MCP server adds an entry under the `mcp` key without replacing your other Kilo settings. Installing an agent or skill creates its own file or directory. Removing an item deletes its marketplace-managed entry from the selected scope. + +{% callout type="warning" title="Keep credentials out of version control" %} +Some MCP servers require API keys, access tokens, or connection strings. Project configuration may be committed to your repository. Prefer environment-variable references for secrets, and review `.kilo/kilo.json` before committing it. +{% /callout %} + +## MCP security and permissions + +An MCP server can expose tools that read data, modify external systems, or run local operations: + +- A **local** MCP server runs a command as a child process on your machine. +- A **remote** MCP server sends requests to an external service. +- Installing the server makes its tools available; it does not automatically approve every tool call. +- MCP tools follow Kilo's `allow`, `ask`, and `deny` permission rules. The default experience may prompt you before a tool runs, depending on your configuration. + +Review the item's author, source link, prerequisites, requested parameters, and available tools before installing it. See [Using MCP in Kilo Code](/docs/automate/mcp/using-in-kilo-code) for configuration, transport, authentication, and permission details. + +## Removing an item + +An item can be installed at both project and global scope. Its Marketplace card shows it as installed and offers a separate remove action for each installed scope. Removing the project copy does not remove the global copy, and vice versa. + +After an install or removal, Kilo reloads the affected configuration. Running sessions may be interrupted so they do not continue with an outdated set of agents, skills, or tools. + +## Contributing + +Marketplace entries are maintained in the [Kilo Marketplace repository](https://github.com/Kilo-Org/kilo-marketplace). Contributions should document prerequisites, parameters, available tools, and any platform-specific requirements. diff --git a/packages/kilo-docs/pages/deploy-secure/index.md b/packages/kilo-docs/pages/deploy-secure/index.md index d929c64349e..91a52cfdac1 100644 --- a/packages/kilo-docs/pages/deploy-secure/index.md +++ b/packages/kilo-docs/pages/deploy-secure/index.md @@ -32,15 +32,6 @@ Ship your applications with one-click deployment: - Real-time log streaming - Deployment history with one-click rollbacks -## Managed Indexing - -Fast, scalable code indexing for better AI context: - -- [**Managed Indexing**](/docs/deploy-secure/managed-indexing) — Cloud-based code indexing -- Improved context for large codebases -- Faster initial indexing times -- Reduced local resource usage - ## Security Reviews AI-powered dependency vulnerability triage for your codebase: @@ -61,8 +52,7 @@ AI-powered dependency vulnerability triage for your codebase: 1. Enable [GitHub Integration](/docs/deploy-secure/deploy#prerequisites) for deployments 2. Set up your first [deployment](/docs/deploy-secure/deploy) in the dashboard -3. Configure [managed indexing](/docs/deploy-secure/managed-indexing) for large projects -4. Enable the [Security Agent](/docs/deploy-secure/security-reviews) to triage your Dependabot alerts +3. Enable the [Security Agent](/docs/deploy-secure/security-reviews) to triage your Dependabot alerts ## Best Practices diff --git a/packages/kilo-docs/pages/deploy-secure/managed-indexing.md b/packages/kilo-docs/pages/deploy-secure/managed-indexing.md deleted file mode 100644 index 1b799c40af2..00000000000 --- a/packages/kilo-docs/pages/deploy-secure/managed-indexing.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: "Managed Indexing" -description: "Cloud-managed codebase indexing" ---- - -# Managed Indexing - -Kilo's **Managed Indexing** feature provides semantic search across your repositories using cloud-hosted embeddings. When enabled, Kilo indexes your codebase to deliver more relevant, context-aware responses during development. - ---- - -## What Managed Indexing Enables - -- Semantic search across your entire codebase -- More accurate and context-aware AI responses -- Git-aware indexing that tracks your base branch and feature branch changes -- Shared indexes for teams and enterprise accounts -- Cost-effective cloud storage with automatic cleanup of stale indexes - ---- - -## Prerequisites - -Before enabling Managed Indexing: - -- **Your workspace must be a Git repository** - Indexing requires a Git repository root directory. Non-Git folders will not be indexed. - -- **Available credit balance** - If your balance reaches zero, managed indexing will be disabled and the extension will revert to local indexing (if configured). - ---- - -## Cost - -- **Currently free during beta** -- **Pricing coming soon** — A daily usage fee for index storage will be deducted from your AI credit balance. You will be charged per GB per day. -- **Embedding model** — Uses `mistralai/codestral-embed-2505` which currently charges $0.15/M input tokens. - ---- - -## How to Enable - -Codebase Indexing is rolling out across our users. It will automatically engage unless your repository root is configured to opt out. - -1. Create a `.kilocode/config.json` file in the root of your repository (if it doesn't already exist). -2. Add the following configuration: - -```json -{ - "project": { - "managedIndexingEnabled": false - } -} -``` - -### Configuration Options - -| Field | Type | Required | Description | -|---|---|---|---| -| `project.id` | string | No | Custom name for your project. Defaults to the name from your Git origin remote. | -| `project.baseBranch` | string | No | Specifies your base branch if it isn't `main`, `master`, `dev`, or `develop`. | -| `project.managedIndexingEnabled` | boolean | No | Set to `false` to disable indexing for individual project repositories. Defaults to `true`. | - -Organization-wide indexing is enabled for any organization that has a credit balance. If you want to disable indexing for a specific repository, set `managedIndexingEnabled` to `false` in the config file. - ---- - -## How Managed Indexing Works - -- **Base branch** — Indexed in its entirety -- **Feature branches** — Only changes from the base branch are indexed -- **Detached HEAD states** — Not indexed -- **Storage** — Embeddings are stored in Kilo Cloud. Your actual code is never stored, only the vector embeddings. -- **Team sharing** — For teams and enterprise accounts, indexes are shared among all team members. - -### Index Retention - -Indexes are stored for **7 days**. If a branch or repository index hasn't been updated within that window, it will be garbage collected. The next time you open the project in VS Code with Kilo running, it will be re-indexed automatically. - -This retention policy keeps costs minimal by only maintaining indexes for actively used code. - ---- - -## Managing Your Indexes - -A minimal UI is available at [app.kilo.ai](https://app.kilo.ai) to: - -- View the size and status of your indexed projects -- Delete old branches & projects. - ---- - -## Migration from Local Indexing - -Enabling managed indexing will **replace local self-hosted indexing entirely**. If you have already configured local indexing for a workspace it will take precedence until you disable it. - -### Automatic Reversion - -If your credit balance reaches zero, the extension will automatically revert to local indexing (if previously configured). - ---- - -## Perfect For - -Managed Indexing is ideal for: - -- **Developers wanting smarter, context-aware AI assistance** -- **Teams needing shared semantic search across repositories** -- **Large codebases where finding relevant code is difficult** -- **Organizations wanting centralized index management** - ---- - -## Limitations and Guidance - -- **Git repository required** — Only Git repository root directories can be indexed. We plan to extend this in the future. -- **Detached HEAD not supported** — Commits in detached HEAD state will not be indexed. -- **7-day retention** — Unused indexes are automatically removed after 7 days. -- **Beta capacity** — During beta, indexing capacity may be limited for very large repositories. -- **Organization indexing** — Shared organization indexes currently require contacting support. diff --git a/packages/kilo-docs/pages/deploy-secure/security-reviews.md b/packages/kilo-docs/pages/deploy-secure/security-reviews.md index bb7f84ba5f0..68d6677aa08 100644 --- a/packages/kilo-docs/pages/deploy-secure/security-reviews.md +++ b/packages/kilo-docs/pages/deploy-secure/security-reviews.md @@ -1,199 +1,491 @@ --- -title: "Security Reviews" -description: "Contextualize dependency vulnerabilities with AI" +title: "Security Agent" +description: "Manage and remediate GitHub Dependabot alerts with AI" --- -# Security Reviews +# Security Agent -Most teams are drowning in Dependabot alerts. The majority of reported CVEs aren't actually exploitable because the vulnerable code path is never used — but figuring that out manually doesn't scale. +Security Agent syncs dependency vulnerability alerts from selected GitHub repositories and stores them as Security Findings in Kilo. It uses AI to triage each finding and runs sandbox analysis when project-specific evidence is needed. It also helps you dismiss or remediate findings, send notifications, and review recorded activity. -Kilo's Security Agent fixes this. It syncs your Dependabot alerts, triages them with AI, and performs deep codebase analysis to determine whether each vulnerability is actually reachable in your code. Non-exploitable findings can be auto-dismissed and synced back to GitHub. - -Available for all users. - ---- +Use Security Agent to identify reachable Dependabot alerts, prioritize findings, and see which findings the analysis recommends dismissing or remediating with a pull request. ## Prerequisites -You need three things before enabling Security Reviews: +Before enabling Security Agent, make sure you have: -1. The [KiloConnect GitHub App](/docs/automate/integrations#connecting-github) installed with `vulnerability_alerts` permission -2. [Dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts) enabled on your target repositories -3. Kilo Code credits for AI model usage +1. The [Kilo GitHub App](/docs/automate/integrations#connecting-github) installed with the `vulnerability_alerts` permission. +2. [Dependabot alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts) enabled on target repositories. +3. Kilo Code credits for AI model usage. ---- +Security Agent currently works with GitHub Dependabot alerts. If the GitHub App loses required permissions, Security Agent prompts you to re-authorize the app before syncing or analyzing findings. ## Get started -1. Go to the **Security Agent** page — either from your [personal dashboard](https://app.kilo.ai/security-agent) or your organization's dashboard -2. Connect GitHub if you haven't already via the [Integrations page](/docs/automate/integrations) -3. Choose which repositories the agent should monitor (all or specific ones) -4. Toggle the agent on — this kicks off an initial sync of your Dependabot alerts +1. Open Security Agent from your personal dashboard or an organization dashboard. +2. If GitHub is not connected, connect it from the Integrations page. +3. Choose a repository scope: all repositories available to the Kilo GitHub App or selected repositories. +4. Turn on Security Agent. +5. Review the General, Automation, Notifications, and SLA settings. -The agent syncs alerts every 6 hours automatically after that. You can trigger a manual sync at any time from the Findings page. +Turning on Security Agent queues an initial sync for the selected repository scope. It then syncs Dependabot alerts every 6 hours. You can also trigger a manual sync from the dashboard or Findings page. ---- +## Organization permissions -## Understand the pipeline +Organization members can view the Security Agent dashboard, findings, and analysis evidence. They can also trigger manual syncs, start or retry analysis, start or retry remediation, and request remediation cancellation. -The Security Agent processes each vulnerability alert through four stages. +Organization owners and billing managers can change Security Agent settings, turn the agent on or off, dismiss findings, clear orphaned findings, and access organization audit reports. Kilo platform admins can also access these reports for audited support and operations. -**Sync** pulls Dependabot alerts from your connected repositories on a 6-hour cycle. +Access to organization notifications is more restricted than access to settings: Security Agent emails go only to current organization owners. -**Triage** runs a quick LLM assessment of the alert metadata — the advisory, severity, package, and version range. Each finding gets classified as **Safe to Dismiss**, **Needs Analysis**, or **Needs Review**. +## How Security Agent works -**Deep analysis** kicks in for findings that warrant it. The Cloud Agent performs a full codebase search for actual usage of the vulnerable package, checks whether the vulnerable code paths are reachable, and suggests fixes when possible. +Security Agent processes dependency vulnerability alerts through several stages. The main finding flow is: -**Auto-dismiss** (when enabled) automatically closes non-exploitable findings and syncs that dismissal back to GitHub with a `[Kilo Code auto-dismiss]` prefix. +```mermaid +flowchart LR + alert["Dependabot alert"] --> sync["Sync into Kilo"] + sync --> triage["AI triage"] + triage --> action["Dismiss, review, or remediate"] + triage -. "When needed" .-> analysis["Sandbox analysis"] + analysis --> action +``` ---- +Notifications and audit recording run alongside this workflow. -## Choose an analysis mode +1. **Sync** pulls Dependabot alerts from repositories in scope and stores them as Security Findings in Kilo. +2. **Triage** quickly assesses advisory metadata with AI, including package, severity, vulnerable range, patched version, and advisory text. +3. **Sandbox analysis** uses Cloud Agent to inspect the codebase when Security Agent needs project-specific evidence. +4. **Auto-dismiss** can dismiss findings that analysis determines are not exploitable, then sync the dismissal back to GitHub. +5. **Remediation** can ask Cloud Agent to create a pull request for an eligible exploitable finding. +6. **Notifications** can email eligible recipients about new findings and SLA events. +7. **Audit reports** show recorded Security Finding activity for a selected reporting period. -You control how much analysis the agent performs via three modes: - -| Mode | What happens | -|---|---| -| **Auto** | Triage first, then deep analysis only when triage recommends it | -| **Shallow** | Triage only — no deep analysis | -| **Deep** | Full codebase analysis for every finding, regardless of triage result | - -**Auto** is the default. It gives you the best balance between thoroughness and credit usage — deep analysis only runs where triage says it's needed. - ---- +Security Agent treats Dependabot alerts as source data and Kilo Security Findings as the working record. A finding can stay open even after a remediation PR exists. The finding closes only when GitHub reports it fixed or someone dismisses it. ## Use the dashboard -The dashboard is the Security Agent's landing page. It gives you a high-level view of your security posture, and every widget links through to the Findings page with the relevant filters applied. Use the repository filter at the top to scope everything to specific repos. +The dashboard shows your current security posture and can be filtered by repository. Use the repository filter to show metrics for one repository or all repositories. -**SLA compliance** is the hero metric — your overall compliance percentage with a per-severity breakdown, linking directly to any overdue findings. +When SLA tracking is enabled, the dashboard shows: -**Severity breakdown** shows open finding counts across Critical, High, Medium, and Low in a 2×2 grid. Click any severity to see those findings. +| Metric | Meaning | +|---|---| +| SLA compliance | Percentage of open findings whose deadlines remain within SLA. | +| Deadline passed | Open findings whose persisted SLA deadlines have passed. | +| Due this week | Open findings approaching their deadline, including confirmed exploitable findings. | +| No deadline | Open findings without evidence of an assigned SLA deadline. | -**Finding status** is a donut chart of Open, Fixed, and Dismissed findings. Click a segment to filter the Findings page. +When SLA tracking is disabled, the dashboard focuses on action posture: -**Analysis coverage** shows a progress bar of analyzed vs. total findings, with an outcome breakdown (Exploitable, Not Exploitable, Safe to Dismiss, etc.). +| Metric | Meaning | +|---|---| +| Open findings | Current open finding count by severity. | +| Confirmed exploitable | Findings where sandbox analysis confirmed project risk. | +| Needs your review | Findings that need a human decision. | +| Analysis not complete | Findings whose project-specific risk is still unknown. | -**Mean time to resolution** compares your average resolution time per severity against your configured SLA targets. - -**Overdue findings** lists the top 10 findings past their SLA deadline — severity, title, repo, package, and how many days overdue. - -**Repository health** is a per-repo summary with severity counts, overdue count, and SLA compliance percentage. - ---- +The dashboard also highlights one finding to address first. It prioritizes overdue findings, findings needing analysis, exploitable findings, and findings needing human review. Dashboard links open the Findings page with matching filters. ## Browse findings -The Findings page is where you work through your vulnerability backlog. At the top, a summary bar shows open/closed counts, your current analysis capacity, when the last sync ran, and a **Sync** button for manual refreshes. +The Findings page is where you work through the vulnerability backlog. -Filter findings by repository, severity, outcome, or sort order to focus on what matters most. Each row shows a severity badge, the finding title and package name, its current outcome label, and an action button — **Analyze**, **Retry**, **Review**, or **View Details** depending on state. +Use filters and sorting to focus the list: -Findings past their SLA deadline are highlighted in red so they're easy to spot. The page paginates at 20 results and auto-refreshes every 5 seconds when analyses are running. +| Control | Options | +|---|---| +| Repository | All repositories or one repository in Security Agent scope. | +| Severity | Critical, High, Medium, Low. | +| Outcome | Not analyzed, Analysis failed, Exploitable, Not exploitable, Safe to dismiss, Needs review, Triage complete, Fixed, Dismissed. | +| Sort | Severity descending, severity ascending, or SLA due date when SLA tracking is enabled. | ---- +Each row shows severity, title, package, analysis outcome, remediation status, and next action. Common actions include Analyze, Retry, Review, Fix, View PR, Retry fix, Cancel, and View details. + +The Findings page shows current analysis capacity and the last sync time. It displays 20 findings per page and refreshes automatically while analysis or remediation is active. ## Inspect a finding -Click any finding to open its detail dialog. There are three tabs. +Click a finding to open its detail dialog, which has three tabs. -The **Details** tab shows the vulnerability metadata — package name and ecosystem, CVE and GHSA IDs, the vulnerable and patched version ranges, manifest path, and a full description. You'll also find a **View on GitHub** link to the original Dependabot alert, plus detection and last sync dates. +### Details -The **Triage** tab shows the agent's initial assessment: a suggested action badge (Safe to Dismiss, Needs Analysis, or Needs Review), a confidence level, and the reasoning behind the decision. If triage hasn't run yet, you can start it here. If it failed, you can retry. +The Details tab shows source vulnerability metadata: -The **Analysis** tab shows the deep analysis results when available — whether the vulnerability is exploitable or not, a summary, up to 5 usage locations found in your codebase, a suggested fix, and full analysis details. There's also a link to continue the investigation in Cloud Agent if you want to dig deeper. +- package name and ecosystem; +- CVE and GHSA identifiers; +- vulnerable version range and patched version; +- manifest path; +- severity and status; +- repository and Dependabot source link; +- detection, sync, and SLA timing when available. ---- +If another finding superseded it, the Details tab links to the current canonical finding. + +### Analysis + +The Analysis tab shows evidence from triage and sandbox analysis. + +Triage can classify a finding as Safe to dismiss, Needs analysis, or Needs review. Depending on the evidence, sandbox analysis can classify a finding as Exploitable, Not exploitable, Monitor, Manual review, or Open PR. + +The Analysis tab shows current progress, failure state, model details, reasoning, usage locations, the suggested fix, and the next action. You can start analysis, retry a failed analysis, restart an active analysis when supported, or start remediation when the finding is eligible. + +### Remediation + +The Remediation tab explains whether Security Agent can start a remediation attempt and why. It also shows remediation history, active attempts, PR outcomes, reasons for failure or blocking, validation evidence, risk notes, cancellation state, and the model used. + +Available remediation actions depend on server-side safety checks. You may see Start remediation, Retry remediation, Cancel remediation, or View PR. ## Understand statuses and outcomes -Every finding has a **primary status** and an **outcome label**. The status tracks the overall lifecycle, while the outcome reflects what the AI determined. - -**Primary status:** +A finding's primary status tracks its source lifecycle: | Status | Meaning | |---|---| -| Open | Active vulnerability that needs attention | -| Fixed | Resolved — detected from the Dependabot alert state | -| Dismissed | Closed by a user or by auto-dismiss | +| Open | Active Security Finding that still needs resolution or dismissal. | +| Fixed | Dependabot reports the alert as fixed. | +| Dismissed | Manual dismissal or auto-dismiss closed the finding and synced the dismissal to GitHub. | +| Superseded | Finding was replaced by a canonical finding after duplicate consolidation. | -**Outcome labels:** +The analysis outcome reflects what the AI analysis determined: | Outcome | Meaning | |---|---| -| Not Analyzed | No analysis has run yet | -| Analyzing | Analysis is currently in progress | -| Analysis Failed | Something went wrong during analysis | -| Exploitable | Deep analysis confirmed it's exploitable | -| Not Exploitable | Deep analysis confirmed it's not reachable | -| Safe to Dismiss | Triage recommends dismissing this finding | -| Needs Review | Triage recommends manual review | -| Triage Complete | Triage is done, no deep analysis needed | +| Not analyzed | No analysis has completed. | +| Queued | Analysis is waiting to run. | +| Analyzing | Analysis is running. | +| Analysis failed | Analysis did not complete. | +| Exploitable | Sandbox analysis confirmed reachable project risk. | +| Not exploitable | Sandbox analysis found no reachable vulnerable path. | +| Safe to dismiss | Triage recommends dismissal. | +| Needs review | Human decision required. | +| Triage complete | Triage finished and no sandbox result is present. | ---- +The remediation status tracks Cloud Agent fix attempts: + +| Status | Meaning | +|---|---| +| Queued | The remediation attempt has been accepted and is waiting to run. | +| Starting | Cloud Agent launch is being prepared. | +| Running | Cloud Agent is working on the remediation. | +| Cancellation requested | User asked Cloud Agent to stop; cancellation is best effort. | +| PR opened | Security Agent verified a remediation PR for the expected repository and branch. | +| No changes needed | Cloud Agent found no code change to make. Finding remains open. | +| Blocked | Security Agent or Cloud Agent could not proceed safely. | +| Failed | Attempt ended with failure. | +| Cancelled | Attempt stopped without opening a PR. | ## Dismiss findings -There are two ways findings get dismissed. +You can dismiss a finding manually from its details. Manual dismissal requires a reason: -**Manually**, you select a finding and choose **Dismiss**. You'll pick a reason — Fix started, No bandwidth, Tolerable risk, Inaccurate, or Not used — and optionally add a comment. The dismissal syncs back to GitHub and closes the corresponding Dependabot alert. +- Fix started +- No bandwidth +- Tolerable risk +- Inaccurate +- Not used -**Automatically**, when auto-dismiss is enabled, the agent closes findings on its own. After deep analysis, any finding determined to be not exploitable is dismissed immediately. After triage, findings with a "dismiss" recommendation are dismissed if they meet your configured confidence threshold. All auto-dismissed alerts are written back to GitHub with a `[Kilo Code auto-dismiss]` prefix. +You can optionally add a comment. A manual dismissal syncs to GitHub and closes the matching Dependabot alert. ---- +When enabled, auto-dismiss automatically dismisses findings that sandbox analysis determines are not exploitable and recommends dismissing. It can also dismiss triage-only findings that recommend dismissal and meet the configured confidence threshold. Security Agent attempts to write each auto-dismissed finding back to GitHub with a `[Kilo Code auto-dismiss]` prefix. See [Configure auto-dismiss](#configure-auto-dismiss) for threshold behavior. -## Configure the agent +## Remediate findings -All settings are on the Security Agent configuration page. +Remediation creates a Security Remediation Attempt and asks Cloud Agent to prepare a fix. If Cloud Agent can make a safe change, it opens a pull request in the affected repository. -**Repository selection** lets you monitor all repositories accessible to the KiloConnect App or pick specific ones from a list. +Starting remediation does not mark the Security Finding fixed. A remediation PR is evidence of work in progress. The finding remains open until Dependabot reports it fixed or someone dismisses it. -**AI models** can be configured separately for triage and deep analysis. The default is Claude Opus 4.6. +### Manual remediation -**Analysis mode** controls the pipeline — Auto (triage then selective deep analysis), Shallow (triage only), or Deep (full analysis on everything). See [Choose an analysis mode](#choose-an-analysis-mode) for details. +You can start manual remediation from an eligible finding even when Auto Remediation is disabled. Manual remediation does not have to meet the Auto Remediation severity threshold. -**Auto-analysis** toggles whether new findings are analyzed automatically. When on, you set a minimum severity threshold (Critical only, High+, Medium+, or All) and whether to include findings that existed before you enabled the feature. +The following safety gates still apply to manual remediation: -**Auto-dismiss** toggles automatic dismissal of non-exploitable findings. You configure a confidence threshold: High only, Medium+, or Any. The "Any" option dismisses at any confidence level — use it with caution. +- the finding is open; +- Security Agent is enabled; +- the repository is still in Security Agent scope; +- sandbox analysis is complete and fresh for the current finding data; +- analysis provides a concrete fix path; +- no active remediation attempt exists; +- no known remediation PR exists for the finding. -**SLA deadlines** set how many days your team has to remediate findings at each severity level: +Manual remediation can proceed when exploitability is unknown or the result requires manual review, provided a concrete fix path is available. Monitor-only findings remain ineligible for one-click remediation. -| Severity | Default | +### Auto remediation + +Auto Remediation is off by default. When enabled, Security Agent can start remediation automatically only for findings that pass all these gates: + +- the finding is open; +- the repository is in Security Agent scope; +- sandbox analysis is complete and fresh; +- the finding is exploitable; +- analysis recommends opening a PR; +- analysis provides a concrete fix path; +- severity meets the configured Auto Remediation threshold; +- there is no active attempt, known PR, or duplicate automatic terminal result. + +Auto Remediation applies to future findings after their analysis completes. If Include existing findings is enabled, it also queues eligible findings that have already been analyzed. Duplicate PRs and duplicate automatic attempts for the same analysis result remain suppressed. + +### Retry and cancel + +You can retry an attempt that failed, was blocked or cancelled, or ended with no changes needed, provided the finding still passes the safety gates and no PR has opened. + +You can cancel a queued or running attempt. Cancellation is best effort. If Cloud Agent opens a verified PR before cancellation completes, Security Agent shows the status as PR opened. + +## Configure Security Agent + +Settings are split into four tabs: General, Automation, Notifications, and SLA. + +### General + +General settings include: + +| Setting | Default | Notes | +|---|---|---| +| Security Agent enabled | Off until you turn it on | Turning it on queues an initial sync for the selected repository scope. | +| Repository selection | Selected repositories during setup | Choose all accessible repositories or selected repositories. | +| Triage model | Kilo Balanced | Used for initial triage and exploitability recommendations. | +| Analysis model | Kilo Balanced | Used for sandbox analysis and result extraction. | +| Remediation model | Kilo Balanced | Used by Cloud Agent for remediation PR work. | +| Analysis mode | Auto | Auto, Shallow, or Deep. | + +#### Turn Security Agent on or off + +Turning on Security Agent checks the GitHub App permissions and queues an initial sync for the current repository scope. Scheduled syncs then run every 6 hours. + +Turning it off stops scheduled syncs and prevents new automatic analysis, remediation, and notification work. It does not delete existing findings or audit history, and it does not cancel a remediation attempt that is already running. Authorized users can still open existing [audit reports](#audit-reports). + +#### Choose repository scope + +Repository scope controls which Dependabot alerts Security Agent syncs: + +- **All repositories** includes every repository currently available to the Kilo GitHub App. +- **Selected repositories** includes only the repositories you choose. + +Changing the scope affects future syncs and remediation eligibility. Removing a repository from scope does not close its existing findings. Open findings remain eligible for SLA tracking until they are fixed, dismissed, superseded, or deleted. If the GitHub App can no longer access a repository, its findings become orphaned and can be removed through [Clear orphaned findings](#clear-orphaned-findings). + +#### Choose models + +Security Agent uses a separate model for each stage: + +- The Triage model reviews advisory metadata and makes the initial exploitability recommendation. +- The Analysis model runs sandbox analysis and extracts the result. +- The Remediation model is used by Cloud Agent to prepare remediation pull requests. + +Kilo Balanced is the default for all three stages. You can change each model independently. The model recorded in finding details is the model used when that analysis or remediation attempt ran. AI triage, sandbox analysis, and remediation consume Kilo Code credits. + +#### Choose an analysis mode + +Analysis mode controls how Security Agent analyzes a finding after syncing it. + +| Mode | What happens | |---|---| -| Critical | 15 days | -| High | 30 days | -| Medium | 45 days | -| Low | 90 days | +| Auto | Run triage first, then sandbox analysis only when triage recommends it. | +| Shallow | Run triage only. Sandbox analysis does not run automatically. | +| Deep | Run sandbox analysis for every finding. | -You can adjust these per your organization's policies and reset to defaults at any time. +Auto is the default. It runs sandbox analysis only when triage determines that codebase-level analysis is needed, which limits sandbox-analysis credit usage. ---- +### Automation + +Automation settings include: + +| Setting | Default | Notes | +|---|---|---| +| Auto-analysis | Off | Automatically analyzes synced findings. | +| Auto-analysis minimum severity | High and above | Critical only, High and above, Medium and above, or All severities. | +| Auto-analysis include existing | Off | Queues previously synced eligible findings. | +| Auto-remediation | Off | Automatically starts remediation for eligible exploitable findings; successful attempts open PRs. | +| Auto-remediation minimum severity | High and above | Critical only, High and above, Medium and above, or All severities. | +| Auto-remediation include existing | Off | Queues already-analyzed eligible findings; duplicate PRs stay suppressed. | +| Auto-dismiss | Off | Automatically dismisses findings that analysis determines are not exploitable and recommends dismissing. | +| Auto-dismiss confidence threshold | High confidence only | High confidence only, Medium or higher, or Any confidence. | + +#### Configure auto-analysis + +Auto-analysis is off by default. When enabled, it queues open findings that are first synced after activation and meet the minimum severity: + +- **Critical only** analyzes critical findings. +- **High and above** analyzes high and critical findings. +- **Medium and above** analyzes medium, high, and critical findings. +- **All severities** analyzes every finding, including findings whose severity is unknown. + +Without Include existing findings, previously synced findings are not added to the automatic-analysis backlog. Turning on Include existing findings queues eligible open findings that already exist in Kilo. Re-enabling Auto-analysis while Include existing findings remains on also checks that backlog. This can use additional credits. + +The selected [analysis mode](#choose-an-analysis-mode) controls the depth of each automatic analysis. Auto-analysis shares per-owner analysis capacity with manual analysis, so a large backlog may take time to finish. + +#### Configure auto-remediation + +Auto-remediation acts only on eligible findings that meet its minimum severity, using the same cumulative severity bands as Auto-analysis. Include existing findings also applies the policy to eligible findings whose sandbox analysis is already complete. Re-enabling Auto-remediation or lowering its severity threshold while Include existing findings is on checks the existing backlog again. Duplicate PRs and duplicate automatic attempts remain suppressed. + +The automatic severity threshold does not restrict manual remediation. See [Auto remediation](#auto-remediation) for the complete eligibility and safety gates, or [Manual remediation](#manual-remediation) for user-triggered fixes. + +#### Configure auto-dismiss + +Auto-dismiss uses the confidence threshold only for triage-only findings that AI recommends dismissing: + +- **High confidence only** accepts only high-confidence recommendations. +- **Medium or higher** accepts medium- and high-confidence recommendations. +- **Any confidence** accepts low-, medium-, and high-confidence recommendations. Use this option with caution. + +The confidence threshold does not apply after sandbox analysis. When sandbox analysis determines that a finding is not exploitable and recommends dismissal, Auto-dismiss can dismiss it regardless of confidence. Enabling Auto-dismiss does not start analysis by itself; it applies when an analysis produces an eligible result. + +Security Agent records the dismissal in Kilo and attempts to sync it to GitHub with a `[Kilo Code auto-dismiss]` prefix. A GitHub write-back failure does not reopen the local finding. See [Dismiss findings](#dismiss-findings) for manual dismissal behavior. + +### Notifications + +The Notifications tab controls New-finding Notifications. + +| Setting | Default | Notes | +|---|---|---| +| New-finding Notifications | Off | Sends an email when Kilo first inserts an eligible open finding. | +| New-finding minimum severity | High and above | Critical only, High and above, Medium and above, or Low and above. | + +#### New-finding notifications + +A New-finding Notification is eligible only when Kilo first inserts an open finding whose severity meets the configured minimum. Severity thresholds are cumulative: for example, High and above includes high and critical findings. Later updates, severity changes, and reopening do not make an existing finding new again. Lowering the severity threshold also does not replay earlier findings. + +Existing Dependabot alerts discovered during the first Security Agent sync count as new because that sync is the first time Kilo inserts them. Enabling New-finding Notifications later does not replay those historical insertions. + +For a personal Security Agent, emails go to the owning user. For an organization Security Agent, they go only to current organization owners. Members and billing managers receive these notifications only if they are also organization owners. See [Notification delivery](#notification-delivery) for delivery checks and deduplication behavior. + +### SLA + +The SLA tab controls remediation deadlines and SLA notifications. + +| Setting | Default | Notes | +|---|---|---| +| SLA tracking | On | Controls SLA dashboard posture and warning or breach eligibility. | +| Critical deadline | 15 days | Editable from 1 to 365 days. | +| High deadline | 30 days | Editable from 1 to 365 days. | +| Medium deadline | 45 days | Editable from 1 to 365 days. | +| Low deadline | 90 days | Editable from 1 to 365 days. | +| SLA notifications | Off | Sends warning and breach emails. | +| SLA notification minimum severity | High and above | Critical only, High and above, Medium and above, or Low and above. | +| SLA warning lead time | 3 days | Whole number from 1 to 365 days. | + +#### How SLA deadlines work + +Security Agent calculates a finding's deadline from the date GitHub first detected the Dependabot alert and the configured number of days for its severity. When a later sync changes the finding's severity or uses updated deadline settings, Security Agent refreshes the persisted deadline. + +Turning off SLA tracking removes SLA posture from the dashboard and makes findings ineligible for SLA Warning and SLA Breach Notifications. It does not close findings, remove their persisted deadlines, or erase recorded activity. Security Agent continues to refresh deadlines when it syncs findings. + +#### SLA notifications + +SLA notifications require both SLA tracking and SLA notifications to be enabled. The minimum severity is cumulative, and warning lead time controls how many whole days before the persisted deadline a warning becomes eligible. See [Notification delivery](#notification-delivery) for exact timing, deduplication, and setting-change behavior. + +## Notification delivery + +Security Agent currently sends notifications only by email. + +Notification kinds: + +| Kind | When eligible | +|---|---| +| New-finding Notification | Kilo first inserts an eligible open finding. | +| SLA Warning Notification | Eligible open finding enters configured warning window before persisted SLA deadline. | +| SLA Breach Notification | Eligible open finding reaches or passes persisted SLA deadline. | + +A finding that is already breached does not receive a stale warning. A warning sent earlier does not suppress the later breach notification. If you enable SLA notifications later, an eligible open finding can produce its current warning or breach during the next evaluation. + +Security Agent creates at most one notification of each kind per finding and recipient. Syncs and sweeps do not intentionally create duplicate notification events. Sent notifications are not replayed when settings change. + +Delivery is asynchronous. Before sending an email, Kilo rechecks the current finding state, Security Agent settings, severity thresholds, SLA state, and recipient authorization. Kilo cancels unsent notification work if the finding is fixed, dismissed, superseded, deleted, or no longer meets the configured threshold, or if the recipient is no longer authorized. + +Email subjects are: + +| Kind | Subject | +|---|---| +| New finding | Kilo Security Agent: New finding | +| SLA warning | Kilo Security Agent: SLA warning | +| SLA breach | Kilo Security Agent: SLA breached | + +Emails include finding severity, repository, title, description, CVE/GHSA/CVSS metadata when available, the SLA deadline for SLA emails, a link to Security Agent findings, and a link to the relevant notification settings. + +## Audit reports + +The audit report shows Security Finding activity recorded for an owner during a selected reporting period. Open it from Security Agent navigation at the applicable route: + +- `/security-agent/audit-report` +- `/organizations/:organizationId/security-agent/audit-report` + +The audit report is based on activity recorded by Kilo. It does not prove that every historical event is present, show repository scan coverage, or provide aggregate SLA compliance. + +### Report period and filters + +By default, the report covers the last 90 calendar days, ending today. A reporting period cannot exceed 90 inclusive calendar days. Kilo rejects future or reversed ranges. + +Filters: + +| Filter | Options | +|---|---| +| Reporting period | Date range up to 90 inclusive calendar days. | +| Severity | All, Critical, High, Medium, Low. | +| Recorded state | All, Open, Fixed, Dismissed, Superseded, Deleted. | +| Repository | All repositories or one repository recorded in report evidence. | + +For each matching finding group, filters retain the complete timeline within the selected period. + +### Report content + +The report starts with summary counts for findings, events, superseded findings, and severity. It then groups activity by Security Finding, combining relevant repository, package, advisory, status, and SLA context with a timeline for the selected period. + +Each timeline event shows when Kilo recorded or applied the activity, who or what performed it, and the evidence needed to understand the outcome. Superseded and deleted findings retain their recorded identity and lifecycle context. + +### Reportable activity + +The audit report includes material Security Finding activity recorded during the selected period: + +- a finding imported into Kilo; +- a severity change; +- a status change, including reopened and fixed; +- manual dismissal, auto-dismissal, supersession, or deletion; +- terminal analysis completion or failure; +- a remediation request; +- remediation ending with PR opened, failed, blocked, cancelled, or no changes needed. + +The audit report excludes reads, page views, unchanged sync observations, queue claims, heartbeats, and retries with no new finding-level outcome. It also excludes notification delivery history, repository scan-coverage appendices, configuration timelines, and report-generation events within the report itself. + +If a report query fails, times out, or exceeds its budget, Kilo returns no report content rather than partial results. Choose a shorter reporting period and generate the report again. + +### Report access + +Personal audit reports are available to the owning user. + +Organization audit reports are available to organization owners, billing managers, and Kilo platform admins. Under current route permissions, organization members who are not owners or billing managers can use other Security Agent surfaces but cannot access organization audit reports. + +Security Agent does not need to be enabled to view an existing report. The report page is available after GitHub integration and the initial Security Agent configuration have been set up. Setup-only states redirect to Settings. ## Clear orphaned findings -If repositories are removed from your GitHub integration or become inaccessible, their findings become orphaned. When this happens, a card appears on the settings page to permanently delete them. +If repositories are removed from GitHub integration or become inaccessible, their findings can become orphaned. The Settings page shows a cleanup card when orphaned repositories exist. {% callout type="warning" %} -Clearing orphaned findings is permanent and cannot be undone. Only do this when you're sure the repositories won't be reconnected. +Clearing orphaned findings permanently deletes findings for the selected repository. You cannot undo this action. Use it only if the repository will not be reconnected. {% /callout %} ---- - ## Compare with Code Reviews -Kilo offers two complementary security features that work best together. +Security Agent and Code Reviews cover different security surfaces. -[**Code Reviews**](/docs/automate/code-reviews/overview) analyzes PR diffs for code quality issues, including security patterns like `innerHTML` usage and hardcoded secrets. It catches problems in new code as it's written. +| Feature | What it covers | +|---|---| +| Code Reviews | Pull request diffs, including security patterns in new code. | +| Security Agent | Dependency vulnerability alerts across selected repositories, including reachability analysis and remediation for Dependabot findings. | -**Security Reviews** takes a different angle — it contextualizes dependency vulnerability alerts across your entire codebase to determine whether Dependabot-reported CVEs are actually exploitable based on how your code uses the affected packages. - -Together, Code Reviews covers your new code surface and Security Reviews covers your dependency vulnerability surface. - ---- +Use Code Reviews to catch risky changes before they are merged. Use Security Agent to manage the dependency vulnerability backlog and determine which Dependabot alerts are exploitable in your codebase. ## Limitations -Security Reviews currently works with **GitHub only** — GitLab support is not yet available. +The following capabilities are not yet implemented but are being considered for upcoming releases of the Security Agent. -The only data source right now is **Dependabot alerts**. Additional sources like npm audit and SBOM analysis are planned. - -There is a **per-account limit** on concurrent analyses. If you have a large backlog, findings will be queued and processed in order. +- GitLab support for Security Agent findings and remediation. +- Security Finding sources beyond Dependabot alerts, such as npm audit and SBOM analysis. +- Notification channels beyond email. +- Historical replay when you enable New-finding Notifications. +- Analysis and remediation without queue delays. This work currently runs through queues and can be delayed by account capacity or worker backlog. +- Automatic finding updates based on the full remediation PR lifecycle. A finding currently closes only when Dependabot reports it fixed or someone dismisses it. +- Planned remediations that combine multiple Security Findings. +- Repository write-permission checks when you configure Security Agent. +- Audit report periods longer than 90 days. +- Complete legacy-history reconstruction and aggregate historical SLA compliance percentages. +- Server-stored report artifacts, PDFs, report caching, and exhaustive notification delivery history. diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 1243bbb4543..adb426172dc 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -97,5 +97,6 @@ Your IAM user or role must have the following permissions: ## Using BYOK in the Extensions and CLI - BYOK works with the Kilo Gateway provider. Users should ensure that is set as the active [provider](/docs/ai-providers). -- Select a model from a provider configured for BYOK, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. +- Kilo Gateway models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. The badge does not apply to models selected through other providers. +- Select a model with the `BYOK` badge, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. - (Optional) Validate with the provider that traffic is being served by that key. diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index a3e9ddd5361..9c2a9be2be7 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -59,6 +59,18 @@ When the Kilo provider is enabled and you are signed in, choose the transcriptio } ``` +### Prompt-Training Model Visibility + +Enable **Hide Prompt-Training Models** under **Models** to remove Kilo Gateway models whose providers may use your prompts for training from model lists. Models from other providers and models without explicit prompt-training metadata remain visible. The setting is disabled by default. + +You can also enable it in `kilo.jsonc`: + +```json +{ + "hide_prompt_training_models": true +} +``` + ### Reasoning Blocks Reasoning blocks stay expanded by default in the VS Code chat UI. Enable **Auto-Collapse Reasoning** in the Display tab, or set `auto_collapse_reasoning` in `kilo.jsonc`, to collapse them after the agent finishes writing them: diff --git a/packages/kilo-docs/pages/index.tsx b/packages/kilo-docs/pages/index.tsx index f95b39d715f..62d29dafc20 100644 --- a/packages/kilo-docs/pages/index.tsx +++ b/packages/kilo-docs/pages/index.tsx @@ -187,7 +187,6 @@ const categories = [ ), links: [ { title: "Deploy", href: "/deploy-secure" }, - { title: "Managed Indexing", href: "/deploy-secure" }, { title: "Security Reviews", href: "/deploy-secure" }, ], }, diff --git a/packages/kilo-docs/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js index e9bb1472ce8..cd3f4d6b326 100644 --- a/packages/kilo-docs/previous-docs-redirects.js +++ b/packages/kilo-docs/previous-docs-redirects.js @@ -613,7 +613,13 @@ module.exports = [ }, { source: "/docs/advanced-usage/managed-indexing", - destination: "/docs/deploy-secure/managed-indexing", + destination: "/docs/customize/context/codebase-indexing", + basePath: false, + permanent: true, + }, + { + source: "/docs/deploy-secure/managed-indexing", + destination: "/docs/customize/context/codebase-indexing", basePath: false, permanent: true, }, diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png index 390e57e4890..0438ce9395f 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/diff-panel-with-diffs-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9d8327c530c9d071b5eb07ebbf48441ea8d64b8a4cd7201665a8fa037ca82ed -size 50349 +oid sha256:e8318f427b6c0ff71d4cce5c8ac03ba4778f99fda5723d24d23c38471523b622 +size 52203 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png index 728ac2a1843..a4234ebfc91 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6ecf78a0bcfaebdb945c3caacb8e32155fe2f6e4017f766e129dd0d17ad8fab -size 51349 +oid sha256:270d3ecab98c4462a0e6af3e8fe43b3313da03759c507914d0b1048045474f1e +size 50886 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png index 81ebcc806e2..17c5f71238b 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:342c39c1ff3d02cef37e076209ddd1d3e1fe0f9eb207097b408d2078c0f05c58 -size 181042 +oid sha256:c87ff987a67d2e1221ca1dffd0c2bbc5b0037853624da6820d2589b238ec98c8 +size 181061 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png deleted file mode 100644 index fd503e50e61..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7338febdf802b5ce501773be164e9da152fb7903e9b16807bd8751fa38f208ed -size 10310 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png deleted file mode 100644 index 071927ae44d..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7e075b4961d3a62c8deda6b481f64cb8777d05183a9c10d0dd2373b7972d3ab -size 53678 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png deleted file mode 100644 index 158e7e0bdcc..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0dd1dc26b28d354ecd14c53dd1b9e6158a161f47152f30299ba4c0827ce1674 -size 50904 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png new file mode 100644 index 00000000000..9bf8b91cdb8 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/empty-list-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6eb9d7670f55c5529dce90d11fceb5c9da7a0d3a5f7270fdfd27e66ca1f3d72f +size 9851 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-empty-chromium-linux.png deleted file mode 100644 index e6db4c0c7e4..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-empty-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1edca81fb2c946f87df5436684418e63b7a9c761079e44f024971a81316b8263 -size 6549 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-installed-chromium-linux.png deleted file mode 100644 index 59da02c6806..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-installed-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce4ba314abb23bb43694d512ade273d99760f250b061e235899d86e170bb5f9a -size 52892 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-items-chromium-linux.png deleted file mode 100644 index 62f2ae9b606..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mcp-tab-with-items-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:631d41ecf6216249943722a797f75c3aefb2f84ebdb7ee80529662a26fa35af5 -size 50206 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png new file mode 100644 index 00000000000..9556137ca56 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/mixed-list-with-items-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a7bcef3102838812625393f0cead3a10067ae32e67dd3f174d0e74d03116c8c +size 54735 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-empty-chromium-linux.png deleted file mode 100644 index 9903c56a8c5..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-empty-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9f722d2782f69170a3efca6ed64ab02d9140f845bb47059ba193e238c768d0a5 -size 4852 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-installed-chromium-linux.png deleted file mode 100644 index d304537721d..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-installed-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c35bc13f78745ab41bf39e732c3945578fc2cb2cc47ef231e51b1357bc0b2817 -size 54160 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-items-chromium-linux.png deleted file mode 100644 index 0bb9c058098..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/skills-tab-with-items-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:52c6eeef2d393b159fe9e88a9d283308d969b7311fe68ea23667561d21a2ed87 -size 51338 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/migration/roo-wizard-selecting-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/migration/roo-wizard-selecting-chromium-linux.png new file mode 100644 index 00000000000..5a1446777b1 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/migration/roo-wizard-selecting-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06c6cd2c2c63745d3cbaec756ae852db3ef0749c005926b75fe3eb03f06b0495 +size 22169 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-thinking-420-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-thinking-420-chromium-linux.png index 6beec18acc5..14bac93660a 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-thinking-420-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-thinking-420-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88d2598f448e68b32b3daef5420a3e31ae5fdafe77ced20aadbc4fdf4c1ab84a -size 4682 +oid sha256:4f395ec688aa5adc800b2145615691ee9bfcc1b5a365600105dd1a17f28e343c +size 6118 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png index aac2e629fe3..0230012032a 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:add00c319d54f53c7c9859428b18182fa91da1b17e4c03b1ee8015ed08f73d19 -size 48653 +oid sha256:4d5db71fbed0542dd8ef3101d7b56b9538b9e6411921150d0957996acd620fde +size 51161 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png index 714c7562e78..21f11b47d28 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9fbce155629b97a30ee9f95f226ef883b6febf8002a657516ff439e3fae0f6c -size 51829 +oid sha256:c501793cb8d0ad50b2d8d283a6314ce4b21dabdc0e95f63174c4a7e475523e32 +size 54055 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-provider-blur-race-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-provider-blur-race-chromium-linux.png index 13e53ad7ee2..041e7f16fee 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-provider-blur-race-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-provider-blur-race-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3330276aded477430e615ababb2caa7eeb2fa7f7406974b9b829e6092d5a619a -size 51792 +oid sha256:8d280dc15c3e64ddf365e1bdaa572d07da0199d0cb9d37acb174ae70db5ae746 +size 57329 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-scope-switch-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-scope-switch-chromium-linux.png index 49e6bf669af..cda24ddb934 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-scope-switch-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-scope-switch-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb78008c0fcbc47cb4b73fcea0739ce975c64b31d58852d74fd31078f3774694 -size 54958 +oid sha256:16b0e2bed3c576328a5565ae957c3af4abbc884a0db894472f587b29e1c355b4 +size 56194 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png index cbcae5fd2ad..37cc654dcef 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/providers-configure-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25f9a2c9499801c7526c6e0dd60a17ecacb873e69e02e7073f33e07b4c96841d -size 28094 +oid sha256:4872106b43a1e7e505d0cb1807fa83455a739ec14e86b65a5369c56afc3e7df6 +size 26645 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png new file mode 100644 index 00000000000..8cde4b3acf0 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/sandboxing-panel-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a817c2b87e1d7b28a7c28deaa8fd8d1e9c51aa2daabda1a2de4a0db0a33fcc17 +size 27768 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 49c8e1c151a..ce7301c2770 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -88,7 +88,6 @@ - - - - @@ -96,6 +95,8 @@ - +- + - - @@ -189,6 +190,8 @@ - +- + - - diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index bcb24a6267a..b755d4912fd 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index d59fb219fc7..598255e66a0 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -37,6 +37,7 @@ const openRouterModelSchema = z.object({ preferredIndex: z.number().optional(), isFree: z.boolean().optional(), mayTrainOnYourPrompts: z.boolean().optional(), + hasUserByokAvailable: z.boolean().optional(), terminalBench: z .object({ overallScore: z.number(), @@ -192,6 +193,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any { tool_call: supportsTools, isFree: model.isFree, mayTrainOnYourPrompts: model.mayTrainOnYourPrompts, + hasUserByokAvailable: model.hasUserByokAvailable, ...(model.terminalBench && { terminalBench: model.terminalBench }), ...(inputPrice !== undefined && outputPrice !== undefined && { diff --git a/packages/kilo-gateway/src/autocomplete.ts b/packages/kilo-gateway/src/autocomplete.ts index a9a5fc09073..51d24b7c2f7 100644 --- a/packages/kilo-gateway/src/autocomplete.ts +++ b/packages/kilo-gateway/src/autocomplete.ts @@ -1,7 +1,7 @@ export type AutocompleteProviderID = "kilo" | "mistral" | "inception" export type DirectAutocompleteProviderID = Exclude -export interface AutocompleteModelDef { +interface AutocompleteModelBase { /** Stable combined value for internal comparisons. */ readonly id: string /** Model value stored in settings and sent to the autocomplete API. */ @@ -18,14 +18,23 @@ export interface AutocompleteModelDef { readonly directProvider?: DirectAutocompleteProviderID /** Request temperature. */ readonly temperature: number - /** - * Which gateway endpoint this model targets. Defaults to "fim" if omitted - * (back-compat with existing entries). Models with `kind: "edit"` route - * through `/kilo/edit` and use Mercury's Next Edit pipeline. - */ - readonly kind?: "fim" | "edit" } +export type AutocompleteModelDef = AutocompleteModelBase & + ( + | { + /** Route through `/kilo/edit` using the Next Edit pipeline. */ + readonly kind: "edit" + /** Stable combined ID of the FIM model used where Next Edit is unsupported. */ + readonly fimModelID: string + } + | { + /** Route through the FIM endpoint. */ + readonly kind?: "fim" + readonly fimModelID?: never + } + ) + const models: AutocompleteModelDef[] = [ { id: "kilo/mistralai/codestral-2508", @@ -57,6 +66,7 @@ const models: AutocompleteModelDef[] = [ requestModel: "inception/mercury-edit-2", temperature: 0, kind: "edit", + fimModelID: "kilo/inception/mercury-edit-2", }, { id: "mistral/codestral-2508", @@ -91,13 +101,14 @@ const models: AutocompleteModelDef[] = [ directProvider: "inception", temperature: 0, kind: "edit", + fimModelID: "inception/mercury-edit-2", }, ] export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models export const DEFAULT_AUTOCOMPLETE_PROVIDER_ID: AutocompleteProviderID = "kilo" -export const DEFAULT_AUTOCOMPLETE_MODEL_ID = "mistralai/codestral-2508" +export const DEFAULT_AUTOCOMPLETE_MODEL_ID = "inception/mercury-next-edit" export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = (() => { const found = models.find( diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index b0a54bded8e..1f52b09c2d6 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -9,7 +9,7 @@ import { getApiKey } from "./auth/token.js" import { buildKiloHeaders, getDefaultHeaders } from "./headers.js" import { ANONYMOUS_API_KEY } from "./api/constants.js" import { resolveKiloOpenRouterBaseUrl } from "./api/url.js" -import { sanitizeResponsesBody } from "./responses.js" +import { transformRequestBody } from "./responses.js" export function buildRequestHeaders(defaultHeaders: Record, requestHeaders?: HeadersInit): Headers { const headers = new Headers(defaultHeaders) @@ -55,7 +55,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = sanitizeResponsesBody(input, init?.body) + const body = transformRequestBody(input, init?.body, options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/responses.ts b/packages/kilo-gateway/src/responses.ts index 3697c5cf775..5ba68266edc 100644 --- a/packages/kilo-gateway/src/responses.ts +++ b/packages/kilo-gateway/src/responses.ts @@ -28,8 +28,13 @@ function strip(input: unknown[]) { return { kept, changed } } -export function sanitizeResponsesBody(input: string | URL | Request, body: BodyInit | null | undefined) { - if (!endpoint(input)) return body +export function transformRequestBody( + input: string | URL | Request, + body: BodyInit | null | undefined, + value?: "allow" | "deny", +) { + const responses = endpoint(input) + if (!responses && !value) return body if (typeof body !== "string") return body const data = (() => { @@ -40,10 +45,14 @@ export function sanitizeResponsesBody(input: string | URL | Request, body: BodyI } })() if (!record(data)) return body - if (data.store === true) return body - if (!Array.isArray(data.input)) return body - const result = strip(data.input) - if (!result.changed) return body - return JSON.stringify({ ...data, input: result.kept }) + const result = responses && data.store !== true && Array.isArray(data.input) ? strip(data.input) : undefined + if (!result?.changed && !value) return body + + const provider = record(data.provider) ? data.provider : {} + return JSON.stringify({ + ...data, + ...(result?.changed ? { input: result.kept } : {}), + ...(value ? { provider: { ...provider, data_collection: value } } : {}), + }) } diff --git a/packages/kilo-gateway/src/tui/types.ts b/packages/kilo-gateway/src/tui/types.ts index fa3a4655c04..95509bbfe2b 100644 --- a/packages/kilo-gateway/src/tui/types.ts +++ b/packages/kilo-gateway/src/tui/types.ts @@ -5,7 +5,6 @@ export interface TUIDependencies { // UI Hooks - useCommandPalette: () => any useSync: () => any useDialog: () => any useToast: () => any diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index e9e8080f02c..ab214d3274c 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -95,6 +95,11 @@ export interface KiloProviderOptions { */ name?: string + /** + * Data collection preference for upstream provider routing + */ + dataCollection?: "allow" | "deny" + /** * Custom fetch function */ diff --git a/packages/kilo-gateway/src/types/tui.d.ts b/packages/kilo-gateway/src/types/tui.d.ts index ddb0e0c0b8f..8bbe5d1f948 100644 --- a/packages/kilo-gateway/src/types/tui.d.ts +++ b/packages/kilo-gateway/src/types/tui.d.ts @@ -3,10 +3,6 @@ * These modules are provided at runtime by the OpenCode TUI system */ -declare module "@tui/context/command-palette" { - export function useCommandPalette(): any -} - declare module "@tui/context/sync" { export function useSync(): any } diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 10ce9ecedb8..56a589af0e5 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -17,6 +17,7 @@ const VALID_RESPONSE = JSON.stringify({ supported_parameters: ["tools", "temperature"], isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }, ], }) @@ -148,6 +149,7 @@ test("returns models without error on success", async () => { expect(result.models["test/model-a"]).toMatchObject({ isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }) }) diff --git a/packages/kilo-gateway/test/autocomplete.test.ts b/packages/kilo-gateway/test/autocomplete.test.ts index 91ba6226b9d..4910979a85b 100644 --- a/packages/kilo-gateway/test/autocomplete.test.ts +++ b/packages/kilo-gateway/test/autocomplete.test.ts @@ -7,11 +7,26 @@ import { } from "../src/autocomplete" describe("DEFAULT_AUTOCOMPLETE_MODEL", () => { - test("resolves to an entry that exists in AUTOCOMPLETE_MODELS", () => { + test("resolves to Mercury Next Edit through Kilo Gateway", () => { const match = AUTOCOMPLETE_MODELS.find( (m) => m.providerID === DEFAULT_AUTOCOMPLETE_PROVIDER_ID && m.modelID === DEFAULT_AUTOCOMPLETE_MODEL_ID, ) + expect(DEFAULT_AUTOCOMPLETE_PROVIDER_ID).toBe("kilo") + expect(DEFAULT_AUTOCOMPLETE_MODEL_ID).toBe("inception/mercury-next-edit") expect(match).toBeDefined() expect(DEFAULT_AUTOCOMPLETE_MODEL).toBe(match!) + expect(DEFAULT_AUTOCOMPLETE_MODEL.kind).toBe("edit") + }) +}) + +describe("Next Edit FIM models", () => { + test("reference a FIM model from the same provider", () => { + for (const model of AUTOCOMPLETE_MODELS) { + if (model.kind !== "edit") continue + const sibling = AUTOCOMPLETE_MODELS.find((candidate) => candidate.id === model.fimModelID) + expect(sibling).toBeDefined() + expect(sibling?.kind).not.toBe("edit") + expect(sibling?.providerID).toBe(model.providerID) + } }) }) diff --git a/packages/kilo-gateway/test/edit.test.ts b/packages/kilo-gateway/test/edit.test.ts index 0d240f70a88..ad21982b7fc 100644 --- a/packages/kilo-gateway/test/edit.test.ts +++ b/packages/kilo-gateway/test/edit.test.ts @@ -29,7 +29,14 @@ describe("Edit target resolution", () => { model: "mistralai/codestral-2508", url: "", }) - expect(resolveEditTarget()).toMatchObject({ provider: "kilo", url: "" }) + }) + + test("routes the default model to the gateway proxy", () => { + expect(resolveEditTarget()).toEqual({ + provider: "kilo", + model: "inception/mercury-edit-2", + url: "https://api.kilo.ai/api/edit/completions", + }) }) }) diff --git a/packages/kilo-gateway/test/responses.test.ts b/packages/kilo-gateway/test/responses.test.ts index 49520300f50..9d73d65c781 100644 --- a/packages/kilo-gateway/test/responses.test.ts +++ b/packages/kilo-gateway/test/responses.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { sanitizeResponsesBody } from "../src/responses" +import { transformRequestBody } from "../src/responses" describe("Responses request sanitization", () => { test("strips item ids when storage is disabled", () => { @@ -30,7 +30,7 @@ describe("Responses request sanitization", () => { ], }) - const result = sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body) const data = JSON.parse(result as string) expect(data.input).toHaveLength(3) @@ -60,19 +60,19 @@ describe("Responses request sanitization", () => { ], }) - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("leaves non-responses requests unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) }) test("leaves invalid responses JSON unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("matches relative responses paths without a placeholder host", () => { @@ -86,9 +86,32 @@ describe("Responses request sanitization", () => { }, ], }) - const result = sanitizeResponsesBody("/api/openrouter/responses?stream=true", body) + const result = transformRequestBody("/api/openrouter/responses?stream=true", body) const data = JSON.parse(result as string) expect(data.input[0].id).toBeUndefined() }) + + test("sanitizes responses and denies data collection in one transform", () => { + const body = JSON.stringify({ + input: [{ type: "message", role: "assistant", id: "msg_tmp_123" }], + provider: { order: ["anthropic"] }, + }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + input: [{ type: "message", role: "assistant" }], + provider: { order: ["anthropic"], data_collection: "deny" }, + }) + }) + + test("denies data collection for non-responses requests", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { data_collection: "deny" }, + }) + }) }) diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 5958cf52994..047744aec3e 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index ccf524599a3..4d8f2381e1a 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-indexing/src/indexing/orchestrator.ts b/packages/kilo-indexing/src/indexing/orchestrator.ts index e842dee17f7..8e2d08ef8bf 100644 --- a/packages/kilo-indexing/src/indexing/orchestrator.ts +++ b/packages/kilo-indexing/src/indexing/orchestrator.ts @@ -277,6 +277,8 @@ export class CodeIndexOrchestrator { private async _runScan(mode: IndexingTelemetryMode, trigger: IndexingTelemetryTrigger): Promise { if (this._cancelRequested) { + if (mode === "incremental") await this.vectorStore.markIndexingComplete() + this.stateManager.setSystemState("Standby", "Indexing cancelled.") log.info("scan skipped: cancellation was requested", { workspacePath: this.workspacePath, mode }) return } @@ -319,6 +321,10 @@ export class CodeIndexOrchestrator { }) if (this._cancelRequested || this.scanner.isCancelled) { + if (mode === "incremental" && result.stats.processed === 0 && batchErrors.length === 0) { + await this.vectorStore.markIndexingComplete() + log.info("preserved unchanged index after cancelled scan", { workspacePath: this.workspacePath }) + } this._isProcessing = false if (this.stateManager.state !== "Error") { this.stateManager.setSystemState("Standby", "Indexing cancelled.") diff --git a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts index 4bef1912c7b..cac76639981 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/orchestrator.test.ts @@ -18,7 +18,9 @@ import { Emitter } from "../../../src/indexing/runtime" class Store { public clearCount = 0 public closeCount = 0 + public completeCount = 0 public deleteCount = 0 + public incompleteCount = 0 constructor( private readonly existing: boolean, @@ -57,8 +59,12 @@ class Store { async hasIndexedData(): Promise { return this.existing } - async markIndexingComplete(): Promise {} - async markIndexingIncomplete(): Promise {} + async markIndexingComplete(): Promise { + this.completeCount += 1 + } + async markIndexingIncomplete(): Promise { + this.incompleteCount += 1 + } } class Scanner { @@ -270,6 +276,31 @@ describe("CodeIndexOrchestrator telemetry", () => { expect(scanner.finished).toBe(true) expect(store.closeCount).toBe(1) + expect(store.incompleteCount).toBe(1) + expect(store.completeCount).toBe(0) + }) + + test("preserves an unchanged index when an incremental scan is interrupted", async () => { + const scanner = new BlockingScanner() + const store = new Store(true) + const orchestrator = new CodeIndexOrchestrator( + createConfig(), + new CodeIndexStateManager(), + "/tmp/ws", + { async clearCacheFile() {}, async flush() {} } as unknown as CacheManager, + store as unknown as IVectorStore, + scanner as unknown as DirectoryScanner, + new Watcher() as unknown as IFileWatcher, + ) + + const active = orchestrator.startIndexing("background") + await scanner.started.promise + await orchestrator.shutdown() + await active + + expect(store.incompleteCount).toBe(1) + expect(store.completeCount).toBe(1) + expect(store.clearCount).toBe(0) }) test("clears stale vectors and hashes before rebuilding an incomplete store", async () => { diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index cb9893acfd3..fd2c68607cc 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## 7.3.47 + +### Patch Changes + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`ef7aa7f`](https://github.com/Kilo-Org/kilocode/commit/ef7aa7fdf854f8a50681b56cb56377fa7763b18d) - Fix JetBrains provider settings after OAuth/connect actions by waiting through transient backend reloads and allowing longer OAuth exchanges. + +- [#11095](https://github.com/Kilo-Org/kilocode/pull/11095) [`9eddaf1`](https://github.com/Kilo-Org/kilocode/commit/9eddaf17126a63822307e9a52d9a32794eca5176) - Highlight shell tool commands in JetBrains chat transcripts. + +- [#11095](https://github.com/Kilo-Org/kilocode/pull/11095) [`5058050`](https://github.com/Kilo-Org/kilocode/commit/50580501679ea0900c2102c0509575e89f15a48e) - Improve markdown readability in JetBrains chat transcripts. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`52dfa54`](https://github.com/Kilo-Org/kilocode/commit/52dfa5453ed6080270f9e07cf6e9cafd8df75cd7) - Hide generated read-tool payload lines from JetBrains prompt bubbles while keeping attachments and assistant tool output visible. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`9f8f698`](https://github.com/Kilo-Org/kilocode/commit/9f8f698e38f8317b0f19d1f7406b0d6cc9777ad9) - Show JetBrains prompt attachments in one horizontal scrolling row in session history. + +- [#11324](https://github.com/Kilo-Org/kilocode/pull/11324) [`bc7f9b0`](https://github.com/Kilo-Org/kilocode/commit/bc7f9b05e8ca61ebf59d5a069923eef59c900a15) - Show a hover copy button for JetBrains session code and tool output blocks. + +- [#11324](https://github.com/Kilo-Org/kilocode/pull/11324) [`358135f`](https://github.com/Kilo-Org/kilocode/commit/358135f69ef6a4985cdd7aab0f1a2a8a0b631c27) - Add copy buttons below JetBrains session prompts and assistant responses. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`db96e31`](https://github.com/Kilo-Org/kilocode/commit/db96e31e655d77ef54ade03c32688218ca2e0e58) - Show provider names on JetBrains model picker buttons for non-Kilo Gateway models. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`49339a2`](https://github.com/Kilo-Org/kilocode/commit/49339a2583f6c51db9d1bfdc3f37ee5a4185b8a9) - Support pasting files and images into JetBrains chat prompts as attachments. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`9f8f698`](https://github.com/Kilo-Org/kilocode/commit/9f8f698e38f8317b0f19d1f7406b0d6cc9777ad9) - Show JetBrains prompt attachments inside the prompt bubble with previews and open embedded attachments in editor tabs. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`a8b127e`](https://github.com/Kilo-Org/kilocode/commit/a8b127e0ca8a7f29a11d03e548e78d084ccc3aa6) - Support sending file and image attachments from the JetBrains chat prompt. + +- [#11275](https://github.com/Kilo-Org/kilocode/pull/11275) [`3c319a5`](https://github.com/Kilo-Org/kilocode/commit/3c319a59a24a7fbf4a1d65eb88d1572ec178694b) - Limit JetBrains prompt input growth to the session while preserving scrolling for long prompts. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`b6bbb83`](https://github.com/Kilo-Org/kilocode/commit/b6bbb839e613a82e65ff445498e128a90851f8a5) - Show Connect for available JetBrains providers without explicit auth metadata, keep only actually configured providers disconnectable, and reduce provider settings diagnostics to debug logs. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`b0183f9`](https://github.com/Kilo-Org/kilocode/commit/b0183f984611df1b09145aa9716e948ee6bb4780) - Prefer remote-safe provider OAuth methods in JetBrains and show device-code authorization details when available. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`d8b6efd`](https://github.com/Kilo-Org/kilocode/commit/d8b6efd58c0ee66b1f71e484282993aad27fc08e) - Show cancellable OAuth progress in JetBrains provider settings and prevent starting another provider action while one is running. + +- [#11278](https://github.com/Kilo-Org/kilocode/pull/11278) [`62e42c1`](https://github.com/Kilo-Org/kilocode/commit/62e42c1efbeecd243c473e3cdde8d8a6ac55efc5) - Stop Kilo backend processes and clear JetBrains UI resources during restartless plugin unloads. + +- [#11324](https://github.com/Kilo-Org/kilocode/pull/11324) [`b4864eb`](https://github.com/Kilo-Org/kilocode/commit/b4864ebd43f211fe3f594edb29798f2f5d48b599) - Fix copying selected text from JetBrains session views. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`793cf93`](https://github.com/Kilo-Org/kilocode/commit/793cf934b72e7f9d71be3f23af058d88be67a9d3) - Support dropping files anywhere in a JetBrains chat session to add them to the prompt. + +- [#11095](https://github.com/Kilo-Org/kilocode/pull/11095) [`bb31723`](https://github.com/Kilo-Org/kilocode/commit/bb31723e7353c0649b9854812f9f803e04d92156) - Polish session header controls and align session view icons. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`709a53c`](https://github.com/Kilo-Org/kilocode/commit/709a53cda17da96a74cbd5a8bb88a9c6a78bd28e) - Open embedded JetBrains message attachments in frontend-managed Kilo editor tabs with loading and connection retry states. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`ad3be6c`](https://github.com/Kilo-Org/kilocode/commit/ad3be6cae999b440277fd8c660ba1b5eead07020) - Organize JetBrains provider settings into connected, popular, and all-provider sections, hide custom-provider creation, and prevent Kilo Gateway disconnects from provider settings. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`a8b127e`](https://github.com/Kilo-Org/kilocode/commit/a8b127e0ca8a7f29a11d03e548e78d084ccc3aa6) - Render file attachments as attachment cards in JetBrains prompts and session history. + +- [#11095](https://github.com/Kilo-Org/kilocode/pull/11095) [`a73ee53`](https://github.com/Kilo-Org/kilocode/commit/a73ee5329cf4455d33d8c8fd363ccf83b46a3cdb) - Render JetBrains shell tool output with markdown code blocks. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`1c7d5ca`](https://github.com/Kilo-Org/kilocode/commit/1c7d5ca7d373770e8d731177e0b851253d9c7d57) - Restore popular provider suggestions in JetBrains provider settings when provider metadata is unavailable. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`987da27`](https://github.com/Kilo-Org/kilocode/commit/987da2728731e1da1c974996b5bcddafe745cea7) - Show shared provider descriptions and provider icons in JetBrains and VS Code provider settings. + +- [#11077](https://github.com/Kilo-Org/kilocode/pull/11077) [`2f9c6ec`](https://github.com/Kilo-Org/kilocode/commit/2f9c6ecdd00c49b9c53a8e72bf8971cabf51821a) - Open embedded transcript attachments in stable Kilo editor tabs. + ## 7.4.0 ### Minor Changes @@ -32,6 +88,40 @@ ## [Unreleased] +## [7.0.1-rc.13] - 2026-06-23 + +### Added + +- Add slash command and file mention completion in the prompt. +- Add support for clickable and explainable `@file` mentions in the prompt. + +### Fixed + +- Fix prompt undo/redo behavior and restore prompt focus after history navigation. +- Fix lazy session creation to avoid duplicate initialization. +- Fix prompt-training model disclosure. + +### Changed + +- Update the bundled CLI to include upstream OpenCode 1.15.13 changes. + +## [7.0.1-rc.12] - 2026-06-18 + +### Added + +- Provider settings management, including searchable provider lists, API-key configuration, OAuth provider login, provider enable/disable controls, disconnect actions, and shared provider metadata. +- Add copy controls to session messages so prompts and assistant responses can be copied directly from the transcript. +- Share codebase indexes across worktrees so Agent Manager and worktree sessions can use semantic search without duplicating the full index. + +### Fixed + +- Keep long JetBrains prompt input usable by capping growth, preserving scrolling, and hiding soft-wrap glyphs. +- Copy actions correctly in session. + +### Changed + +- Update the bundled CLI runtime to OpenCode 1.15.9 + ## [7.0.1-rc.11] - 2026-06-17 ### Added diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt index b31f8884192..19a399cdca5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendChatManager.kt @@ -168,7 +168,8 @@ class KiloBackendChatManager( val raw = response.body?.string() log.warn("prompt_async failed: HTTP $code") raw?.let { log.debug { "${ChatLogSummary.sid(id)} kind=prompt op=prompt_async error=${ChatLogSummary.body(it)}" } } - throw RuntimeException("prompt_async failed: HTTP $code") + val detail = raw?.takeIf { it.isNotBlank() }?.let { ": ${ChatLogSummary.body(it)}" }.orEmpty() + throw RuntimeException("prompt_async failed: HTTP $code$detail") } log.debug { "${ChatLogSummary.sid(id)} kind=prompt op=prompt_async ok=true code=$code" } } @@ -180,6 +181,12 @@ class KiloBackendChatManager( } } + fun command(id: String, dir: String, command: String, args: String, prompt: PromptDto) { + log.info("${ChatLogSummary.sid(id)} kind=command command=$command args=${args.length} parts=${prompt.parts.size}") + val body = KiloCliDataParser.buildCommandJson(command, args, prompt) + post("/session/$id/command?directory=${encode(dir)}", body, "command", "${ChatLogSummary.sid(id)} kind=command command=$command") + } + // ------ abort ------ fun abort(id: String, dir: String) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index 5ad06704ee7..0cbaf9659d5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -88,17 +88,18 @@ class KiloBackendCliManager( val platform = platform() val exe = if (SystemInfo.isWindows) "kilo.exe" else "kilo" val target = File(PathManager.getSystemPath(), "kilo/bin/$exe") - val snapshot = File(target.parentFile, "models-snapshot.json") + val worker = File(target.parentFile, "kilo-sandbox-mutation-worker.js") if (forceExtract) { log.info("Force re-extracting CLI resources under ${target.parentFile.absolutePath}") if (target.exists()) target.delete() - if (snapshot.exists()) snapshot.delete() + if (worker.exists()) worker.delete() forceExtract = false } extractResource("cli/$platform/$exe", target, executable = true) - extractResource("cli/$platform/models-snapshot.json", snapshot, executable = false) + if (worker.exists()) worker.delete() + extractResource("cli/$platform/kilo-sandbox-mutation-worker.js", worker, executable = false) return target } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 02a63765b33..f6140172aba 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -23,6 +23,8 @@ import ai.kilocode.rpc.dto.ModelLimitDto import ai.kilocode.rpc.dto.ModelSelectionDto import ai.kilocode.rpc.dto.ModelStateDto import ai.kilocode.rpc.dto.PartDto +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionFileDiffDto import ai.kilocode.rpc.dto.PermissionReplyDto @@ -586,6 +588,7 @@ object KiloCliDataParser { part.mime?.let { fields += "\"mime\":${escape(it)}" } part.url?.let { fields += "\"url\":${escape(it)}" } part.filename?.let { fields += "\"filename\":${escape(it)}" } + part.source?.let { fields += "\"source\":${sourceJson(it)}" } return "{${fields.joinToString(",")}}" } fields += "\"text\":${escape(part.text.orEmpty())}" @@ -598,6 +601,24 @@ object KiloCliDataParser { fun buildSummarizeJson(model: ModelSelectionDto): String = """{"providerID":${escape(model.providerID)},"modelID":${escape(model.modelID)}}""" + fun buildCommandJson(command: String, args: String, prompt: PromptDto): String { + val fields = mutableListOf( + "\"command\":${escape(command)}", + "\"arguments\":${escape(args)}", + ) + prompt.agent?.let { fields += "\"agent\":${escape(it)}" } + prompt.variant?.let { fields += "\"variant\":${escape(it)}" } + val pid = prompt.providerID + val mid = prompt.modelID + if (pid != null && mid != null) { + val model = "$pid/$mid" + fields += "\"model\":${escape(model)}" + } + val parts = prompt.parts.filter { it.type == "file" }.joinToString(",") { buildPromptPartJson(it) } + if (parts.isNotEmpty()) fields += "\"parts\":[$parts]" + return "{${fields.joinToString(",")}}" + } + /** * Build the partial JSON body for `PATCH /global/config`. */ @@ -772,6 +793,8 @@ object KiloCliDataParser { mime = obj.str("mime"), url = obj.str("url"), filename = obj.str("filename"), + synthetic = obj.flagOrNull("synthetic"), + source = parseSource(obj["source"]), tool = obj.str("tool"), callID = obj.str("callID"), state = state?.str("status"), @@ -799,6 +822,37 @@ object KiloCliDataParser { return READ_TOOL_PATH.containsMatchIn(line) } + private fun parseSource(raw: JsonElement?): PartSourceDto? { + val obj = raw.obj() ?: return null + val type = obj.str("type") ?: return null + val text = obj["text"].obj() ?: return null + val value = text.str("value") ?: return null + val start = text.num("start") ?: return null + val end = text.num("end") ?: return null + return PartSourceDto( + type = type, + text = PartSourceTextDto(value = value, start = start, end = end), + path = obj.str("path"), + clientName = obj.str("clientName"), + uri = obj.str("uri"), + name = obj.str("name"), + kind = obj.long("kind")?.safeInt(), + ) + } + + private fun sourceJson(source: PartSourceDto): String { + val fields = mutableListOf( + "\"type\":${escape(source.type)}", + "\"text\":{\"value\":${escape(source.text.value)},\"start\":${source.text.start},\"end\":${source.text.end}}", + ) + source.path?.let { fields += "\"path\":${escape(it)}" } + source.clientName?.let { fields += "\"clientName\":${escape(it)}" } + source.uri?.let { fields += "\"uri\":${escape(it)}" } + source.name?.let { fields += "\"name\":${escape(it)}" } + source.kind?.let { fields += "\"kind\":$it" } + return "{${fields.joinToString(",")}}" + } + internal fun parseTodos(raw: JsonElement?): List { return parseTodosOrNull(raw) ?: emptyList() } @@ -988,10 +1042,12 @@ object KiloCliDataParser { temperature = model.temperature, toolCall = model.toolCall, free = model.free, + byok = model.byok, status = model.status, recommendedIndex = model.recommendedIndex, variants = model.variants, limit = model.limit?.let { ModelLimitDto(it.context, it.input, it.output) }, + mayTrainOnYourPrompts = model.mayTrainOnYourPrompts, ) } @@ -1006,6 +1062,7 @@ object KiloCliDataParser { temperature = cap.bool("temperature"), toolCall = cap.bool("toolcall"), free = obj.bool("isFree"), + byok = obj.bool("hasUserByokAvailable"), status = obj.str("status"), recommendedIndex = obj.num("recommendedIndex"), variants = parseVariants(obj), @@ -1016,6 +1073,7 @@ object KiloCliDataParser { output = it.long("output") ?: 0, ) }, + mayTrainOnYourPrompts = obj.bool("mayTrainOnYourPrompts"), ) } @@ -1289,6 +1347,11 @@ private fun JsonObject.flag(key: String, default: Boolean): Boolean { return prim.booleanOrNull ?: prim.contentOrNull?.toBooleanStrictOrNull() ?: default } +private fun JsonObject.flagOrNull(key: String): Boolean? { + val prim = this[key]?.jsonPrimitive ?: return null + return prim.booleanOrNull ?: prim.contentOrNull?.toBooleanStrictOrNull() +} + private fun Long.safeInt() = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() private fun JsonObject?.map(key: String): Map { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 156d2336639..0f079d2c097 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -110,6 +110,12 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi { chat.prompt(id, directory, prompt) } + override suspend fun command(id: String, directory: String, command: String, arguments: String, prompt: PromptDto) { + app.requireReady() + LOG.info("command RPC: session=$id, dir=$directory, command=$command, parts=${prompt.parts.size}") + chat.command(id, directory, command, arguments, prompt) + } + override suspend fun abort(id: String, directory: String) = ready { chat.abort(id, directory) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index fc6c4246b99..08559711ff0 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -74,10 +74,12 @@ internal object KiloWorkspaceDtoMapper { temperature = m.temperature, toolCall = m.toolCall, free = m.free, + byok = m.byok, status = m.status, recommendedIndex = m.recommendedIndex, variants = m.variants, limit = m.limit?.let { ModelLimitDto(it.context, it.input, it.output) }, + mayTrainOnYourPrompts = m.mayTrainOnYourPrompts, ) private fun agent(a: AgentInfo) = AgentDto( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt index 8c82b37bb8b..5b54f93cfa8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt @@ -1,5 +1,3 @@ -@file:Suppress("UnstableApiUsage") - package ai.kilocode.backend.rpc import ai.kilocode.backend.app.KiloAppState @@ -16,19 +14,36 @@ import ai.kilocode.log.KiloLog import ai.kilocode.jetbrains.api.model.Agent import ai.kilocode.rpc.KiloWorkspaceRpcApi import ai.kilocode.rpc.dto.ConfigTargetDto +import ai.kilocode.rpc.dto.FileSearchResultDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto import ai.kilocode.rpc.dto.ModelsWorkspaceDto import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.ide.actions.searcheverywhere.FoundItemDescriptor +import com.intellij.ide.util.gotoByName.ChooseByNameInScopeItemProvider +import com.intellij.ide.util.gotoByName.ChooseByNamePopup +import com.intellij.ide.util.gotoByName.ChooseByNameViewModel +import com.intellij.ide.util.gotoByName.GotoFileModel import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.readAction import com.intellij.openapi.components.service +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.navigation.NavigationItem +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.indexing.FindSymbolParameters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -46,6 +61,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.InvalidPathException import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.resume /** @@ -63,6 +79,8 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { private val LEGACY = listOf("opencode.jsonc", "opencode.json") private val GLOBAL = MODERN + LEGACY + "config.json" private val LOCAL_DIRS = listOf(".kilo", ".kilocode", ".opencode") + private const val SEARCH_CAP = 2_000 + private const val DIFF_CAP = 200_000 private val CONFIG = """{ "${'$'}schema": "$SCHEMA" } @@ -71,6 +89,8 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { private val app: KiloBackendAppService get() = service() + private val gitCache = ConcurrentHashMap() + private val manager: KiloBackendWorkspaceManager get() = app.workspaces @@ -173,6 +193,31 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { return found.values.toList() } + override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto { + val base = file(clean(directory) ?: directory) ?: return FileSearchResultDto() + val git = withContext(Dispatchers.IO) { gitAvailable(base) } + val project = project(base) ?: return FileSearchResultDto(git = git) + if (DumbService.getInstance(project).isDumb) return FileSearchResultDto(indexing = true, git = git) + return try { + val files = readAction { search(project, base, query, limit.coerceIn(1, 200)) } + FileSearchResultDto(files = files, git = git) + } catch (e: IndexNotReadyException) { + FileSearchResultDto(indexing = true, git = git) + } catch (e: LinkageError) { + LOG.warn("file search API unavailable; returning no suggestions", e) + FileSearchResultDto(git = git) + } + } + + override suspend fun gitChanges(directory: String): String? = withContext(Dispatchers.IO) { + val base = file(clean(directory) ?: directory) ?: return@withContext null + if (!gitAvailable(base)) return@withContext null + val unstaged = git(base, "diff") + val staged = git(base, "diff", "--staged") + val text = listOf(unstaged, staged).filter { it.isNotBlank() }.joinToString("\n") + text.takeIf { it.isNotBlank() }?.take(DIFF_CAP) + } + override suspend fun openFile(path: String): Boolean { val item = clean(path) ?: return false val target = file(item)?.takeIf { it.isAbsolute } ?: return false @@ -240,15 +285,9 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { } private fun clean(path: String): String? { - val raw = path.trim().takeIf { it.isNotBlank() } ?: return null - return try { - val cut = raw.substringBefore('#').substringBefore('?') - val decoded = if (cut.startsWith("file:")) URI(cut).path else URLDecoder.decode(cut, StandardCharsets.UTF_8) - Path.of(decoded.replace('\\', '/')).normalize().toString() - } catch (e: Exception) { - LOG.debug { "Failed to normalize workspace file path: $path (${e.message})" } - null - } + val result = normalizeWorkspacePath(path) + if (result == null && path.isNotBlank()) LOG.debug { "Failed to normalize workspace file path: $path" } + return result } private fun file(path: String): Path? = try { @@ -273,6 +312,91 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { } ?: projects.firstOrNull() } + // Uses the IDE Go-to-File engine (com.intellij.ide.util.gotoByName.*). These are public but + // unstable lang-impl classes (not @ApiStatus.Internal) -- the same engine behind Search Everywhere, + // chosen for proven large-repo performance. searchFiles() degrades gracefully on LinkageError. + @Suppress("UnstableApiUsage") + private fun search(project: Project, base: Path, query: String, limit: Int): List { + val text = query.trim() + if (text.isBlank()) return roots(project, base, limit) + val scope = GlobalSearchScope.projectScope(project) + val model = object : GotoFileModel(project) { + override fun acceptItem(item: NavigationItem): Boolean { + val psi = item as? PsiFileSystemItem ?: return false + val path = file(psi.virtualFile.path) ?: return false + return path.startsWith(base) && super.acceptItem(item) + } + + override fun loadInitialCheckBoxState(): Boolean = false + + override fun saveInitialCheckBoxState(state: Boolean) {} + } + val view = object : ChooseByNameViewModel { + override fun getProject(): Project = project + + override fun getModel() = model + + override fun isSearchInAnyPlace(): Boolean = model.useMiddleMatching() + + override fun transformPattern(pattern: String): String = ChooseByNamePopup.getTransformedPattern(pattern, model) + + override fun canShowListForEmptyPattern(): Boolean = false + + override fun getMaximumListSizeLimit(): Int = limit + } + val provider = model.getItemProvider(null) + val params = FindSymbolParameters.wrap(text, scope) + val found = mutableListOf>() + val indicator = EmptyProgressIndicator() + if (provider is ChooseByNameInScopeItemProvider) { + provider.filterElementsWithWeights(view, params, indicator) { item -> + found += item + found.size < SEARCH_CAP + } + } else { + provider.filterElements(view, text, false, indicator) { item -> + found += FoundItemDescriptor(item, 0) + found.size < SEARCH_CAP + } + } + return found.asSequence() + .sortedByDescending { it.weight } + .mapNotNull { item -> (item.item as? PsiFileSystemItem)?.virtualFile } + .mapNotNull { vf -> fileDto(base, vf) } + .distinctBy { it.path } + .take(limit) + .toList() + } + + private fun roots(project: Project, base: Path, limit: Int): List { + val root = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(base) ?: return emptyList() + val index = ProjectFileIndex.getInstance(project) + return root.children.asSequence() + .filter { it.name != ".git" } + .filterNot { index.isExcluded(it) } + .mapNotNull { fileDto(base, it) } + .sortedWith( + compareByDescending { it.directory } + .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, + ) + .take(limit) + .toList() + } + + private fun fileDto(base: Path, vf: VirtualFile): WorkspaceFileDto? { + val path = file(vf.path) ?: return null + val rel = relativeWithinBase(base, path) ?: return null + return WorkspaceFileDto(rel, vf.name, vf.isDirectory) + } + + private fun gitAvailable(base: Path): Boolean { + return workspaceGitAvailable(base, gitCache) + } + + private fun git(base: Path, vararg args: String): String { + return runWorkspaceGit(base, *args) + } + private fun agent(a: Agent) = AgentInfo( name = a.name, displayName = a.displayName, @@ -309,3 +433,42 @@ class KiloWorkspaceRpcApiImpl : KiloWorkspaceRpcApi { } private fun encode(value: String) = URLEncoder.encode(value, Charsets.UTF_8) + +internal fun normalizeWorkspacePath(path: String): String? { + val raw = path.trim().takeIf { it.isNotBlank() } ?: return null + return try { + val cut = raw.substringBefore('#').substringBefore('?') + val decoded = if (cut.startsWith("file:")) URI(cut).path else URLDecoder.decode(cut, StandardCharsets.UTF_8) + Path.of(decoded.replace('\\', '/')).normalize().toString() + } catch (_: Exception) { + null + } +} + +internal fun workspaceGitAvailable(base: Path, cache: ConcurrentHashMap = ConcurrentHashMap()): Boolean { + if (Files.exists(base.resolve(".git"))) return true + return cache.getOrPut(base.toString()) { + runWorkspaceGit(base, "rev-parse", "--is-inside-work-tree").trim() == "true" + } +} + +internal fun runWorkspaceGit(base: Path, vararg args: String): String { + return try { + val cmd = GeneralCommandLine(listOf("git") + args).withWorkDirectory(base.toFile()) + val out = CapturingProcessHandler(cmd).runProcess(5_000) + out.stdout.takeIf { !out.isTimeout && out.exitCode == 0 }.orEmpty() + } catch (_: Exception) { + "" + } +} + +/** + * Relativizes [target] against [base], returning the forward-slash relative path, or null if + * [target] is not strictly inside [base] (path-traversal guard) or equals the base itself. + */ +internal fun relativeWithinBase(base: Path, target: Path): String? { + val path = target.normalize() + if (!path.startsWith(base)) return null + val rel = base.relativize(path).toString().replace('\\', '/') + return rel.ifBlank { null } +} diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index b21b4bf707f..66127633cd1 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -54,10 +54,12 @@ data class ModelInfo( val temperature: Boolean, val toolCall: Boolean, val free: Boolean, + val byok: Boolean = false, val status: String?, val recommendedIndex: Double?, val variants: List, val limit: ModelLimitInfo?, + val mayTrainOnYourPrompts: Boolean = false, ) data class ModelLimitInfo( diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt index a38b13d106a..29b0ec03507 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendChatManagerTest.kt @@ -3,6 +3,8 @@ package ai.kilocode.backend.app import ai.kilocode.backend.testing.MockCliServer import ai.kilocode.backend.testing.TestLog import ai.kilocode.rpc.dto.ModelSelectionDto +import ai.kilocode.rpc.dto.PromptDto +import ai.kilocode.rpc.dto.PromptPartDto import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -75,6 +77,22 @@ class KiloBackendChatManagerTest { assertEquals("Enhance prompt failed: HTTP 500", error.message) } + @Test + fun `prompt failure includes CLI response body summary`() { + val port = mock.start() + val chat = KiloBackendChatManager(scope, TestLog()) + chat.start(OkHttpClient(), port, MutableSharedFlow()) + mock.promptStatus = 400 + mock.promptResponse = """{"issues":[{"message":"invalid source type"}]}""" + + val error = assertFailsWith { + chat.prompt("ses_abc", "/test/project", PromptDto(parts = listOf(PromptPartDto(type = "text", text = "hello")))) + } + + assertTrue(error.message!!.contains("prompt_async failed: HTTP 400"), error.message) + assertTrue(error.message!!.contains("chars="), error.message) + } + @Test fun `enhance prompt cancels the HTTP request with its coroutine`() = runBlocking { val port = mock.start() diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt index 8419d47340d..26950ce09f1 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt @@ -29,15 +29,18 @@ class KiloBackendSessionManagerTest { private val mock = MockCliServer() private val log = TestLog() private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val apps = mutableListOf() @AfterTest fun tearDown() { + apps.forEach { it.dispose() } + apps.clear() scope.cancel() mock.close() } private fun setup(): KiloBackendAppService { - return KiloBackendAppService.create(scope, FakeCliServer(mock), log) + return KiloBackendAppService.create(scope, FakeCliServer(mock), log).also { apps.add(it) } } private suspend fun ready(app: KiloBackendAppService) { diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt index b19ed6072dd..a457d308835 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt @@ -128,7 +128,8 @@ class GeneratedApiModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -139,6 +140,7 @@ class GeneratedApiModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 5c824749af5..3d96d257108 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -10,6 +10,8 @@ import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionReplyDto import ai.kilocode.rpc.dto.ModelSelectionDto import ai.kilocode.rpc.dto.ModelStateDto +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto import ai.kilocode.rpc.dto.PromptDto import ai.kilocode.rpc.dto.PromptPartDto import ai.kilocode.rpc.dto.QuestionReplyDto @@ -190,6 +192,42 @@ class KiloCliDataParserTest { assertEquals("a.png", result.part.filename) } + @Test + fun `parseChatEvent - part preserves synthetic flag and source metadata`() { + val data = globalEvent(""" + "type": "message.part.updated", + "properties": { + "sessionID": "ses_1", + "part": { + "id": "file_1", + "sessionID": "ses_1", + "messageID": "msg_1", + "type": "file", + "mime": "text/plain", + "url": "file:///tmp/a.kt", + "filename": "a.kt", + "synthetic": true, + "source": { + "type": "file", + "path": "src/a.kt", + "text": { "value": "@src/a.kt", "start": 4, "end": 13 } + } + } + } + """) + + val result = KiloCliDataParser.parseChatEvent("message.part.updated", data) + + assertNotNull(result) + assertTrue(result is ChatEventDto.PartUpdated) + assertEquals(true, result.part.synthetic) + assertEquals("file", result.part.source?.type) + assertEquals("src/a.kt", result.part.source?.path) + assertEquals("@src/a.kt", result.part.source?.text?.value) + assertEquals(4.0, result.part.source?.text?.start) + assertEquals(13.0, result.part.source?.text?.end) + } + @Test fun `ChatEventNormalizer - user part updated sanitizes text`() { val norm = KiloCliDataParser.ChatEventNormalizer() @@ -1112,6 +1150,25 @@ class KiloCliDataParserTest { ) } + @Test + fun `parseMessages - preserves synthetic and source metadata`() { + val raw = """[ + { + "info": { "id": "m1", "sessionID": "s1", "role": "user", "time": { "created": 1.0 } }, + "parts": [ + { "id": "p1", "sessionID": "s1", "messageID": "m1", "type": "text", "text": "hidden", "synthetic": true }, + { "id": "f1", "sessionID": "s1", "messageID": "m1", "type": "file", "mime": "text/plain", "url": "file:///tmp/a.kt", "source": { "type": "file", "path": "src/a.kt", "text": { "value": "@src/a.kt", "start": 0, "end": 9 } } } + ] + } + ]""" + + val result = KiloCliDataParser.parseMessages(raw).single() + + assertEquals(true, result.parts[0].synthetic) + assertEquals("src/a.kt", result.parts[1].source?.path) + assertEquals("@src/a.kt", result.parts[1].source?.text?.value) + } + @Test fun `parseMessages - message with tool parts`() { val raw = """[{ @@ -1254,6 +1311,9 @@ class KiloCliDataParserTest { }, "limit": {"context": 200000, "input": 100000, "output": 16000}, "status": "active", + "isFree": false, + "hasUserByokAvailable": true, + "mayTrainOnYourPrompts": true, "recommendedIndex": 2, "variants": {"high": {}, "low": {}, "medium": {}}, "options": {}, "headers": {} @@ -1273,6 +1333,9 @@ class KiloCliDataParserTest { assertTrue(model.temperature) assertTrue(model.toolCall) assertEquals("active", model.status) + assertFalse(model.free) + assertTrue(model.byok) + assertTrue(model.mayTrainOnYourPrompts) assertEquals(2.0, model.recommendedIndex) assertEquals(200000L, model.limit?.context) assertEquals(100000L, model.limit?.input) @@ -1322,7 +1385,13 @@ class KiloCliDataParserTest { "extra": true }, "unknown": "ok", - "models": {} + "models": { + "gpt-5": { + "name": "GPT-5", + "capabilities": {}, + "mayTrainOnYourPrompts": true + } + } }], "default": {"code":"openai/gpt-5"}, "connected": ["openai"] @@ -1335,6 +1404,7 @@ class KiloCliDataParserTest { assertEquals("Build with OpenAI models", provider.description) assertEquals("openai", provider.metadata?.icon) assertEquals(3, provider.metadata?.priority) + assertTrue(provider.models.getValue("gpt-5").mayTrainOnYourPrompts) assertEquals(listOf("openai"), result.second) assertEquals(mapOf("code" to "openai/gpt-5"), result.third) } @@ -1368,6 +1438,7 @@ class KiloCliDataParserTest { assertEquals(false, model.reasoning) assertEquals(false, model.temperature) assertEquals(false, model.toolCall) + assertFalse(model.mayTrainOnYourPrompts) assertNull(model.limit) } @@ -1621,6 +1692,86 @@ class KiloCliDataParserTest { assertTrue(result.contains(""""filename":"a \"b\".txt""""), result) } + @Test + fun `buildPromptJson - file part includes source metadata`() { + val prompt = PromptDto(parts = listOf(PromptPartDto( + type = "file", + mime = "text/plain", + url = "file:///tmp/a.kt", + filename = "a.kt", + source = PartSourceDto( + type = "file", + path = "src/a.kt", + text = PartSourceTextDto("@src/a.kt", 4.0, 13.0), + ), + ))) + + val result = KiloCliDataParser.buildPromptJson(prompt) + + assertEquals( + """{"parts":[{"type":"file","mime":"text/plain","url":"file:///tmp/a.kt","filename":"a.kt","source":{"type":"file","text":{"value":"@src/a.kt","start":4.0,"end":13.0},"path":"src/a.kt"}}]}""", + result, + ) + } + + @Test + fun `buildPromptJson - data file part includes source metadata`() { + val prompt = PromptDto(parts = listOf(PromptPartDto( + type = "file", + mime = "text/plain", + url = "data:text/plain;charset=utf-8,diff%20content", + filename = "git-changes.txt", + source = PartSourceDto( + type = "file", + text = PartSourceTextDto("@git-changes", 7.0, 19.0), + path = "git-changes", + ), + ))) + + val result = KiloCliDataParser.buildPromptJson(prompt) + + assertEquals( + """{"parts":[{"type":"file","mime":"text/plain","url":"data:text/plain;charset=utf-8,diff%20content","filename":"git-changes.txt","source":{"type":"file","text":{"value":"@git-changes","start":7.0,"end":19.0},"path":"git-changes"}}]}""", + result, + ) + } + + @Test + fun `buildCommandJson - file part includes source metadata`() { + val prompt = PromptDto(parts = listOf(PromptPartDto( + type = "file", + mime = "text/plain", + url = "file:///tmp/a.kt", + source = PartSourceDto( + type = "file", + path = "src/a.kt", + text = PartSourceTextDto("@src/a.kt", 0.0, 9.0), + ), + ))) + + val result = KiloCliDataParser.buildCommandJson("review", "", prompt) + + assertTrue(result.contains(""""source":{"type":"file","text":{"value":"@src/a.kt","start":0.0,"end":9.0},"path":"src/a.kt"}"""), result) + } + + @Test + fun `buildCommandJson - includes agent variant model and arguments`() { + val prompt = PromptDto( + parts = emptyList(), + agent = "code", + variant = "high", + providerID = "kilo", + modelID = "gpt-5", + ) + + val result = KiloCliDataParser.buildCommandJson("review", "src/", prompt) + + assertEquals( + """{"command":"review","arguments":"src/","agent":"code","variant":"high","model":"kilo/gpt-5"}""", + result, + ) + } + // ---- buildSummarizeJson ---- @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt index 35ba6f52e34..d9ad9a7c96f 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt @@ -111,7 +111,8 @@ class ProjectModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -122,6 +123,7 @@ class ProjectModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapperTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapperTest.kt new file mode 100644 index 00000000000..dfccc531876 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapperTest.kt @@ -0,0 +1,44 @@ +package ai.kilocode.backend.rpc + +import ai.kilocode.backend.workspace.ModelInfo +import ai.kilocode.backend.workspace.ProviderData +import ai.kilocode.backend.workspace.ProviderInfo +import kotlin.test.Test +import kotlin.test.assertTrue + +class KiloWorkspaceDtoMapperTest { + + @Test + fun `providers preserve prompt training disclosure`() { + val model = ModelInfo( + id = "paid", + name = "Paid", + attachment = false, + reasoning = false, + temperature = false, + toolCall = true, + free = false, + status = null, + recommendedIndex = null, + variants = emptyList(), + limit = null, + mayTrainOnYourPrompts = true, + ) + val data = ProviderData( + providers = listOf( + ProviderInfo( + id = "kilo", + name = "Kilo", + source = "api", + models = mapOf(model.id to model), + ), + ), + connected = listOf("kilo"), + defaults = emptyMap(), + ) + + val result = KiloWorkspaceDtoMapper.providers(data) + + assertTrue(result.providers.single().models.getValue("paid").mayTrainOnYourPrompts) + } +} diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/WorkspacePathScopingTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/WorkspacePathScopingTest.kt new file mode 100644 index 00000000000..673220ad08d --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/WorkspacePathScopingTest.kt @@ -0,0 +1,150 @@ +package ai.kilocode.backend.rpc + +import kotlinx.coroutines.runBlocking +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class WorkspacePathScopingTest { + // Derive an absolute, OS-portable base from the real home dir so the test runs identically on + // Windows, macOS, and Linux (no hardcoded POSIX "/home/..." literals). + private val base: Path = Path.of(System.getProperty("user.home")).resolve("kilo-scope-test").normalize() + + private fun at(vararg segments: String): Path = segments.fold(base) { acc, s -> acc.resolve(s) } + + @Test + fun `in-base file returns forward-slash relative path`() { + assertEquals("src/A.kt", relativeWithinBase(base, at("src", "A.kt"))) + } + + @Test + fun `nested file returns nested relative path`() { + assertEquals("a/b/c.kt", relativeWithinBase(base, at("a", "b", "c.kt"))) + } + + @Test + fun `base itself is rejected as blank`() { + assertNull(relativeWithinBase(base, base)) + } + + @Test + fun `sibling directory outside base is rejected`() { + assertNull(relativeWithinBase(base, base.resolveSibling("other").resolve("A.kt"))) + } + + @Test + fun `parent directory is rejected`() { + assertNull(relativeWithinBase(base, base.parent)) + } + + @Test + fun `traversal that escapes base is rejected after normalization`() { + assertNull(relativeWithinBase(base, base.resolve("..").resolve("secret").resolve("A.kt"))) + } + + @Test + fun `traversal that stays inside base is kept after normalization`() { + assertEquals("src/A.kt", relativeWithinBase(base, base.resolve("x").resolve("..").resolve("src").resolve("A.kt"))) + } + + @Test + fun `prefix sibling is not treated as inside base`() { + val sibling = base.resolveSibling(base.fileName.toString() + "-2") + assertNull(relativeWithinBase(base, sibling.resolve("A.kt"))) + } + + @Test + fun `normalizes encoded file URLs`() { + val path = base.resolve("dir with spaces").resolve("A.kt") + val url = path.toUri().toString() + "?query#fragment" + + assertEquals(path.normalize().toString(), normalizeWorkspacePath(url)) + } + + @Test + fun `normalizes escaped relative paths`() { + assertEquals("src/A.kt", normalizeWorkspacePath("src%2Ftmp%2F..%2FA.kt")) + } + + @Test + fun `rejects blank and invalid paths`() { + assertNull(normalizeWorkspacePath(" ")) + assertNull(normalizeWorkspacePath("file://%")) + } + + @Test + fun `git availability detects temp repository`() { + val dir = repo() ?: return + try { + assertTrue(workspaceGitAvailable(dir)) + } finally { + delete(dir) + } + } + + @Test + fun `git changes returns capped large diff without blocking`() = runBlocking { + val dir = repo() ?: return@runBlocking + try { + val file = dir.resolve("large.txt") + file.writeText("base\n") + git(dir, "add", "large.txt") + git(dir, "commit", "-m", "base") + file.writeText((1..40_000).joinToString("\n") { "line-$it" } + "\n") + + val diff = assertNotNull(KiloWorkspaceRpcApiImpl().gitChanges(dir.toString())) + assertTrue(diff.startsWith("diff --git")) + assertEquals(200_000, diff.length) + } finally { + delete(dir) + } + } + + @Test + fun `git changes returns null outside git repositories`() = runBlocking { + val dir = Files.createTempDirectory("kilo-non-git") + try { + assertNull(KiloWorkspaceRpcApiImpl().gitChanges(dir.toString())) + } finally { + delete(dir) + } + } + + private fun repo(): Path? { + if (!gitInstalled()) return null + val dir = Files.createTempDirectory("kilo-git") + git(dir, "init") + git(dir, "config", "user.email", "test@example.com") + git(dir, "config", "user.name", "Test User") + return dir + } + + private fun gitInstalled(): Boolean { + return try { + ProcessBuilder("git", "--version").start().waitFor() == 0 + } catch (_: Exception) { + false + } + } + + private fun git(dir: Path, vararg args: String) { + val proc = ProcessBuilder(listOf("git") + args) + .directory(dir.toFile()) + .redirectErrorStream(true) + .start() + val out = proc.inputStream.bufferedReader().readText() + val code = proc.waitFor() + assertEquals(0, code, out) + } + + private fun delete(dir: Path) { + Files.walk(dir).use { paths -> + paths.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } + } +} diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 078cf52c7d0..f9380d9c897 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -93,6 +93,10 @@ class MockCliServer : AutoCloseable { @Volatile var summarizeStatus = 200 @Volatile var lastSummarizePath: String? = null @Volatile var lastSummarizeBody: String? = null + @Volatile var promptStatus = 200 + @Volatile var promptResponse = "true" + @Volatile var lastPromptPath: String? = null + @Volatile var lastPromptBody: String? = null @Volatile var enhanced = """{"text":"Enhanced prompt"}""" @Volatile var enhanceStatus = 200 @Volatile var lastEnhancePath: String? = null @@ -342,11 +346,11 @@ class MockCliServer : AutoCloseable { bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) - bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "GET" -> + bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" -> respond(output, sessionGetStatus, sessionCreate) - bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "DELETE" -> + bare.matches(Regex("/session/ses_[^/]+")) && method == "DELETE" -> respond(output, sessionDeleteStatus, "true") - bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "PATCH" -> { + bare.matches(Regex("/session/ses_[^/]+")) && method == "PATCH" -> { lastSessionRenamePath = path lastSessionRenameBody = body lastSessionRenameMethod = method @@ -357,6 +361,11 @@ class MockCliServer : AutoCloseable { lastSummarizeBody = body respond(output, summarizeStatus, summarizeResponse) } + bare.matches(Regex("/session/ses_[^/]+/prompt_async")) && method == "POST" -> { + lastPromptPath = path + lastPromptBody = body + respond(output, promptStatus, promptResponse) + } bare == "/enhance-prompt" && method == "POST" -> { lastEnhancePath = path lastEnhanceBody = body diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspaceTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspaceTest.kt index 6decdb82e85..44c2330303e 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspaceTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspaceTest.kt @@ -2,11 +2,15 @@ package ai.kilocode.backend.workspace import ai.kilocode.backend.app.KiloAppState import ai.kilocode.backend.app.KiloBackendAppService +import ai.kilocode.backend.app.KiloBackendSessionManager +import ai.kilocode.backend.app.SseEvent +import ai.kilocode.backend.cli.KiloBackendHttpClients import ai.kilocode.backend.workspace.KiloBackendWorkspace import ai.kilocode.backend.workspace.KiloWorkspaceState import ai.kilocode.backend.testing.FakeCliServer import ai.kilocode.backend.testing.MockCliServer import ai.kilocode.backend.testing.TestLog +import ai.kilocode.jetbrains.api.client.DefaultApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -14,6 +18,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -31,15 +36,18 @@ class KiloBackendWorkspaceTest { private val mock = MockCliServer() private val log = TestLog() private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val apps = mutableListOf() @AfterTest fun tearDown() { + apps.forEach { it.dispose() } + apps.clear() scope.cancel() mock.close() } private fun setup(): KiloBackendAppService = - KiloBackendAppService.create(scope, FakeCliServer(mock), log) + KiloBackendAppService.create(scope, FakeCliServer(mock), log).also { apps.add(it) } private suspend fun ready(app: KiloBackendAppService): KiloBackendWorkspace { app.connect() @@ -296,11 +304,14 @@ class KiloBackendWorkspaceTest { @Test fun `agents response filters hidden and subagent`() = runBlocking { + mock.providers = PROVIDERS_JSON mock.agents = """[ {"name":"code","mode":"primary","permission":[],"options":{}}, {"name":"helper","mode":"subagent","permission":[],"options":{}}, {"name":"secret","mode":"primary","hidden":true,"permission":[],"options":{}} ]""" + mock.commands = COMMANDS_JSON + mock.skills = SKILLS_JSON val app = setup() val ws = ready(app) @@ -382,20 +393,27 @@ class KiloBackendWorkspaceTest { @Test fun `concurrent get for same directory returns same instance`() = runBlocking { - val app = setup() - app.connect() - withTimeout(10_000) { app.appState.first { it is KiloAppState.Ready } } + val port = mock.start() + val http = KiloBackendHttpClients.api(mock.password) + val api = DefaultApi(basePath = "http://127.0.0.1:$port", client = http) + val events = MutableSharedFlow() + val sessions = KiloBackendSessionManager(scope, log) + val manager = KiloBackendWorkspaceManager(scope, sessions, log) + manager.start(api, http, port, events) - // Launch many concurrent get() calls for the same directory - val results = (1..10).map { - async(Dispatchers.Default) { - app.workspaces.get("/same/dir") - } - }.awaitAll() + try { + val results = (1..10).map { + async(Dispatchers.Default) { + manager.get("/same/dir") + } + }.awaitAll() - // All must return the exact same instance - val first = results[0] - results.forEach { assertTrue(it === first) } + val first = results[0] + results.forEach { assertTrue(it === first) } + } finally { + manager.stop() + KiloBackendHttpClients.shutdown(http) + } } @Test diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/CheckCliTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/CheckCliTask.kt index 5ad5b22e00d..dffe15e5169 100644 --- a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/CheckCliTask.kt +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/CheckCliTask.kt @@ -45,7 +45,7 @@ abstract class CheckCliTask : DefaultTask() { val missing = platforms.get().filter { platform -> val d = File(resolved, platform) val exe = if (platform.startsWith("windows")) "kilo.exe" else "kilo" - !File(d, exe).exists() || !File(d, "models-snapshot.json").exists() + !File(d, exe).exists() } if (missing.isNotEmpty()) { throw GradleException( diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/PrepareLocalCliTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/PrepareLocalCliTask.kt index 0a65e8f4dbd..dc1501154f7 100644 --- a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/PrepareLocalCliTask.kt +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/PrepareLocalCliTask.kt @@ -26,8 +26,8 @@ import javax.inject.Inject * 4. Each entry on `PATH` * 5. Common install locations (`~/.bun/bin`, `/opt/homebrew/bin`, `/usr/local/bin`) * - * After Bun finishes, the task verifies the expected binary and models snapshot exist so that - * Rosetta / architecture mismatches surface with a clear error message. + * After Bun finishes, the task verifies the expected binary exists so that Rosetta / architecture + * mismatches surface with a clear error message. */ @DisableCachingByDefault(because = "Local developer bootstrap task that shells out to Bun") abstract class PrepareLocalCliTask : DefaultTask() { @@ -47,8 +47,7 @@ abstract class PrepareLocalCliTask : DefaultTask() { @TaskAction fun run() { val expected = binary() - val snapshot = snapshot() - if (expected.exists() && snapshot.exists()) { + if (expected.exists()) { logger.lifecycle("CLI binary already exists at ${expected.absolutePath}") return } @@ -72,19 +71,12 @@ abstract class PrepareLocalCliTask : DefaultTask() { "This can happen if Bun and Gradle are running under different architectures." ) } - if (!snapshot.exists()) { - throw GradleException("Expected CLI models snapshot was not created at ${snapshot.absolutePath}.") - } } private fun binary(): File { return File(File(dir.get().asFile, platform()), exe()) } - private fun snapshot(): File { - return File(File(dir.get().asFile, platform()), "models-snapshot.json") - } - private fun platform(): String { return "${os()}-${arch()}" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index 00e8a6be944..41a054592cb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -171,6 +171,12 @@ class KiloSessionService internal constructor( LOG.info("${ChatLogSummary.sid(id)} kind=prompt ok=true") } + suspend fun command(id: String, dir: String, command: String, args: String, dto: PromptDto) { + LOG.info("${ChatLogSummary.sid(id)} kind=command command=$command parts=${dto.parts.size}") + call { command(id, dir, command, args, dto) } + LOG.info("${ChatLogSummary.sid(id)} kind=command ok=true") + } + /** Abort ongoing processing for a session. */ suspend fun abort(id: String, dir: String) { call { abort(id, dir) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index a414a0adb2b..3b9558bdbd7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -4,6 +4,7 @@ package ai.kilocode.client.app import ai.kilocode.rpc.KiloWorkspaceRpcApi import ai.kilocode.rpc.dto.ConfigTargetDto +import ai.kilocode.rpc.dto.FileSearchResultDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto import ai.kilocode.rpc.dto.LoadErrorDto @@ -13,6 +14,7 @@ import com.intellij.openapi.components.Service import ai.kilocode.log.KiloLog import fleet.rpc.client.durable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flow @@ -126,6 +128,26 @@ class KiloWorkspaceService internal constructor( } } + suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto { + return try { + call { searchFiles(directory, query, limit) } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + LOG.warn("workspace file search failed for directory=$directory query=$query", e) + FileSearchResultDto() + } + } + + suspend fun gitChanges(directory: String): String? { + return try { + call { gitChanges(directory) } + } catch (e: Exception) { + LOG.warn("git changes lookup failed for directory=$directory", e) + null + } + } + suspend fun openPath(directory: String, path: String): Boolean { val match = files(directory, path).firstOrNull() ?: return false return try { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index d880bc32225..f89e60583db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -35,6 +35,11 @@ class SessionSidePanelManager( private val status: () -> Map = { project.service().activity() }, private val history: ((Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, private val timers: UiTimerSource = UiTimers, + private val request: (JComponent) -> Unit = { focus -> + ApplicationManager.getApplication().invokeLater({ + IdeFocusManager.getInstance(project).requestFocusInProject(focus, project) + }, ModalityState.defaultModalityState()) + }, ) : SessionManager, Disposable { val component: JPanel = object : JPanel(BorderLayout()), DataProvider { override fun getData(dataId: String): Any? { @@ -120,7 +125,7 @@ class SessionSidePanelManager( val view = cached ?: createHistory().also { panel = it } if (cached != null && view is HistoryPanel) view.refresh() if (current == null && component.componentCount == 1 && component.getComponent(0) === view) { - focusHistory(view) + focus((view as? HistoryPanel)?.defaultFocusedComponent) return } current = null @@ -128,14 +133,12 @@ class SessionSidePanelManager( component.add(view, BorderLayout.CENTER) component.revalidate() component.repaint() - focusHistory(view) + focus((view as? HistoryPanel)?.defaultFocusedComponent) } - private fun focusHistory(view: JComponent) { - val focus = (view as? HistoryPanel)?.defaultFocusedComponent ?: return - ApplicationManager.getApplication().invokeLater({ - IdeFocusManager.getInstance(project).requestFocusInProject(focus, project) - }, ModalityState.defaultModalityState()) + private fun focus(component: JComponent?) { + val focus = component ?: return + request(focus) } private fun createHistory(): JComponent { @@ -181,6 +184,7 @@ class SessionSidePanelManager( component.add(ui, BorderLayout.CENTER) component.revalidate() component.repaint() + focus(ui.defaultFocusedComponent) } private fun register(ui: SessionUi?) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 8a47e19af4a..bce1c8e2eb5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -8,6 +8,7 @@ import ai.kilocode.client.migration.KiloMigrationService import ai.kilocode.client.migration.MigrationUiController import ai.kilocode.client.migration.MigrationUiState import ai.kilocode.client.migration.ui.MigrationOverlayPanel +import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.FileAttachment import ai.kilocode.client.session.model.SessionModelEvent import ai.kilocode.client.session.model.SessionState @@ -18,7 +19,11 @@ import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.mode.ModePicker import ai.kilocode.client.session.ui.model.ModelPicker +import ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProvider +import ai.kilocode.client.session.ui.prompt.MentionAction import ai.kilocode.client.session.ui.prompt.PromptPanel +import ai.kilocode.client.session.ui.prompt.SlashAction +import ai.kilocode.client.session.ui.prompt.mentionParts as promptMentionParts import ai.kilocode.client.session.ui.account.SessionAccountOverlay import ai.kilocode.client.session.ui.SessionDropOverlay import ai.kilocode.client.session.ui.SessionRootPanel @@ -40,6 +45,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.LoginRequiredView import ai.kilocode.client.session.views.permission.PermissionView import ai.kilocode.client.session.views.question.QuestionView +import ai.kilocode.client.settings.KiloSettingsConfigurable import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.client.ui.layout.Stack @@ -67,6 +73,7 @@ import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ConfigurableWithId import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.registry.Registry import com.intellij.util.concurrency.annotations.RequiresEdt @@ -168,6 +175,7 @@ class SessionUi( private lateinit var connection: ConnectionPanel private lateinit var prompt: PromptPanel + private lateinit var completion: KiloPromptCompletionProvider private lateinit var load: LoadingPanel private lateinit var migrationOverlay: MigrationOverlayPanel private var empty: EmptySessionPanel? = null @@ -336,12 +344,21 @@ class SessionUi( scroll.onScroll = overlay::clear connection = ConnectionPanel(this, controller) + completion = KiloPromptCompletionProvider( + workspace = workspace, + service = workspaces, + actions = slashActions(), + mentions = mentionActions(), + scope = cs, + ) prompt = PromptPanel( project = project, selection = selection, onSend = { text, files -> sendPrompt(text, files) }, onAbort = { controller.abort() }, onEnhance = controller::enhancePrompt, + onMentions = ::mentionParts, + completion = completion, ) drop = SessionDropOverlay() @@ -398,15 +415,17 @@ class SessionUi( }, m.agent) val items = m.models.map { ModelPicker.Item( - it.id, - it.display, - it.provider, - it.providerName, - it.recommendedIndex, - it.free, - it.variants, - it.attachment, - ) + id = it.id, + display = it.display, + provider = it.provider, + providerName = it.providerName, + recommendedIndex = it.recommendedIndex, + free = it.free, + byok = it.byok, + variants = it.variants, + attachment = it.attachment, + mayTrainOnYourPrompts = it.mayTrainOnYourPrompts, + ) } val selected = m.model?.let { full -> items.firstOrNull { it.key == full }?.key } @@ -415,6 +434,7 @@ class SessionUi( prompt.reasoning.setItems(m.variants.map { ReasoningPicker.Item(it, variantTitle(it)) }, m.variant) prompt.setResetVisible(m.modelOverride) prompt.setReady(m.isReady()) + prompt.refreshHighlights() } is SessionControllerEvent.ViewChanged.ShowProgress -> { @@ -588,10 +608,63 @@ class SessionUi( } prompt.clear() val follow = scroll.atBottom() + val action = completion.clientAction(text) + if (action != null) { + action.action() + scroll.followBottom(follow) + return + } + val command = completion.serverCommand(text) + if (command != null) { + controller.command(command.first, command.second, files) + scroll.followBottom(follow) + return + } controller.prompt(text, files) scroll.followBottom(follow) } + private fun slashActions(): List { + val fns: Map Unit> = mapOf( + SlashAction.NEW to { manager?.newSession() }, + SlashAction.SESSIONS to { manager?.showHistory() }, + SlashAction.MODELS to { prompt.model.open() }, + SlashAction.AGENTS to { prompt.mode.open() }, + SlashAction.VARIANT to { prompt.reasoning.open() }, + SlashAction.COMPACT to { controller.compact() }, + SlashAction.SETTINGS to { openKiloSettings() }, + SlashAction.HELP to { BrowserUtil.browse("https://kilo.ai/docs") }, + ) + return SlashAction.ALL.map { spec -> bind(spec, fns.getValue(spec)) } + } + + private fun bind(spec: SlashAction.Spec, action: () -> Unit) = SlashAction( + spec.name, + KiloBundle.message(spec.descriptionKey), + spec.hints, + action, + ) + + private fun mentionActions(): List = MentionAction.ALL.map(::bind) + + private fun bind(spec: MentionAction.Spec) = MentionAction( + spec.name, + KiloBundle.message(spec.descriptionKey), + spec.hints, + spec.available, + ) + + private fun mentionParts(text: String): List = runBlockingCancellable { + val names = MentionAction.ALL.mapTo(mutableSetOf()) { it.name } + promptMentionParts( + text = text, + directory = workspace.directory, + reserved = names, + resolve = { path -> workspaces.files(workspace.directory, path).isNotEmpty() }, + gitChanges = { workspaces.gitChanges(workspace.directory) }, + ) + } + private fun openFile(path: String) { cs.launch { workspaces.openPath(workspace.directory, path) @@ -706,6 +779,16 @@ class SessionUi( ) } + private fun openKiloSettings() { + ShowSettingsUtil.getInstance().showSettingsDialog( + project, + Predicate { cfg: Configurable -> + cfg is ConfigurableWithId && cfg.getId() == KiloSettingsConfigurable.ID + }, + { _: Configurable -> }, + ) + } + override fun dispose() { disposed = true hide.stop() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 7048f8e9aa7..53dfcde9504 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -54,6 +54,7 @@ import ai.kilocode.log.KiloLog import com.intellij.openapi.util.Disposer import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -97,6 +98,14 @@ class SessionController( private data class OrganizationTarget(val org: String?) private data class Followup(val dir: String, val time: Long) private data class Pref(val agent: String?, val model: String?, val variants: List, val variant: String?, val reset: Boolean) + private data class Dispatch( + val kind: String, + val source: String, + val text: String, + val props: Map, + val start: String, + val exists: Boolean, + ) companion object { private val LOG = KiloLog.create(SessionController::class.java) @@ -134,6 +143,7 @@ class SessionController( private var tool: String? = null private var eventJob: Job? = null private var drainJob: Job? = null + private var creating: CompletableDeferred? = null private val childJobs: MutableMap = mutableMapOf() private val childIds: MutableSet = mutableSetOf() private var sessionLoadState: SessionLoadState = SessionLoadState.Idle @@ -239,40 +249,41 @@ class SessionController( val dto = promptDto(text, files) val props = promptProps(files) LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(dto)} ${ChatLogSummary.dir(directory)}" } + dispatch(Dispatch("prompt", "user", text, props, start, exists)) { id -> + sessions.prompt(id, directory, dto) + } + } + + fun command(command: String, args: String, files: List = emptyList()) { + assertEdt() + val start = sid ?: ref?.key ?: "pending" + val exists = sid != null + val dto = promptDto("", files) + val props = promptProps(files) + LOG.debug { "${ChatLogSummary.sid(start)} kind=command command=$command args=${args.length} ${ChatLogSummary.dir(directory)}" } + dispatch(Dispatch("command", "command", args, props, start, exists)) { id -> + sessions.command(id, directory, command, args, dto) + } + } + + private fun dispatch(data: Dispatch, send: suspend (String) -> Unit) { + assertEdt() capture("Conversation Send Clicked", sessionProps(sid ?: ref?.key) + mapOf( - "source" to "user", - "hasExistingSession" to exists.toString(), - "textLength" to bucket(text), - ) + props) + "source" to data.source, + "hasExistingSession" to data.exists.toString(), + "textLength" to bucket(data.text), + ) + data.props) showSession() + val pending = sid?.let { CompletableDeferred(it) } ?: session() cs.launch { try { - val id = sid ?: run { - val session = sessions.create(directory) - runEdt { - if (disposed) return@runEdt - ref = SessionRef.Local(session) - setRecentSessionsState(RecentsState.Idle) - updateModel { - model.setSession(session) - } - } - if (disposed) return@launch - val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session" - LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true") - capture("Task Created", sessionProps(session.id) + mapOf("source" to "jetbrains")) - runEdt { - if (disposed) return@runEdt - subscribeEvents() - } - session.id - } - sessions.prompt(id, directory, dto) - capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString()) + props) - LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } + val id = pending.await() ?: return@launch + send(id) + capture("Conversation Message", sessionProps(id) + mapOf("source" to data.source, "hasExistingSession" to data.exists.toString()) + data.props) + LOG.debug { "${ChatLogSummary.sid(id)} kind=${data.kind} dispatched=true" } } catch (e: Exception) { - capture("Session Error", sessionProps(sid ?: ref?.key ?: start) + mapOf("context" to "prompt", "errorClass" to e::class.java.name)) - LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) + capture("Session Error", sessionProps(sid ?: ref?.key ?: data.start) + mapOf("context" to data.kind, "errorClass" to e::class.java.name)) + LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: data.start)} kind=${data.kind} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { if (disposed) return@edt val msg = e.message ?: KiloBundle.message("session.error.prompt") @@ -284,6 +295,47 @@ class SessionController( } } + private fun session(): CompletableDeferred { + assertEdt() + val pending = creating + if (pending != null) return pending + val next = CompletableDeferred() + creating = next + cs.launch { + try { + next.complete(createSession()) + } catch (e: Exception) { + next.completeExceptionally(e) + } finally { + edt { + if (creating === next) creating = null + } + } + } + return next + } + + private suspend fun createSession(): String? { + val session = sessions.create(directory) + runEdt { + if (disposed) return@runEdt + ref = SessionRef.Local(session) + setRecentSessionsState(RecentsState.Idle) + updateModel { + model.setSession(session) + } + } + if (disposed) return null + val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session" + LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true") + capture("Task Created", sessionProps(session.id) + mapOf("source" to "jetbrains")) + runEdt { + if (disposed) return@runEdt + subscribeEvents() + } + return session.id + } + fun abort() { assertEdt() LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=abort" } @@ -657,16 +709,18 @@ class SessionController( .flatMap { provider -> provider.models.map { (id, info) -> ModelItem( - id, - info.name, - provider.id, - provider.name, - info.recommendedIndex, - info.free, - info.variants, - info.limit?.let { ModelLimitItem(it.context, it.input, it.output) }, - info.attachment, - ) + id = id, + display = info.name, + provider = provider.id, + providerName = provider.name, + recommendedIndex = info.recommendedIndex, + free = info.free, + byok = info.byok, + variants = info.variants, + limit = info.limit?.let { ModelLimitItem(it.context, it.input, it.output) }, + attachment = info.attachment, + mayTrainOnYourPrompts = info.mayTrainOnYourPrompts, + ) } } } ?: emptyList() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt index adf6fd84915..e2ab924b547 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.session.model import ai.kilocode.rpc.dto.MessageDto +import ai.kilocode.rpc.dto.PartSourceDto import ai.kilocode.rpc.dto.PartTimeDto import ai.kilocode.rpc.dto.TodoDto import ai.kilocode.rpc.dto.TodoViewDto @@ -71,6 +72,7 @@ class FileAttachment(id: String) : Content(id) { var mime: String = "application/octet-stream" var url: String = "" var filename: String? = null + var source: PartSourceDto? = null } /** Tool invocation with lifecycle state. */ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 79eeb432f8f..87ae423c204 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -41,6 +41,7 @@ class SessionModel { private val entries = LinkedHashMap() private val turnEntries = LinkedHashMap() + private val hiddenText = mutableSetOf>() var app: KiloAppStateDto = KiloAppStateDto(KiloAppStatusDto.DISCONNECTED) var version: String? = null @@ -140,6 +141,7 @@ class SessionModel { @RequiresEdt fun removeMessage(id: String) { if (entries.remove(id) == null) return + hiddenText.removeAll { it.first == id } fire(SessionModelEvent.MessageRemoved(id)) regroup() updateHeader() @@ -147,6 +149,7 @@ class SessionModel { @RequiresEdt fun removeContent(messageId: String, contentId: String) { + hiddenText.remove(messageId to contentId) val msg = entries[messageId] ?: return if (msg.parts.remove(contentId) == null) return fire(SessionModelEvent.ContentRemoved(messageId, contentId)) @@ -157,6 +160,16 @@ class SessionModel { fun updateContent(messageId: String, dto: PartDto) { if (dto.type in SILENT_PART_TYPES) return val msg = entries[messageId] ?: return + val key = messageId to dto.id + if (hiddenSynthetic(msg, dto)) { + hiddenText.add(key) + if (msg.parts.remove(dto.id) != null) { + fire(SessionModelEvent.ContentRemoved(messageId, dto.id)) + updateHeader() + } + return + } + hiddenText.remove(key) val existing = msg.parts[dto.id] if (empty(dto)) { if (existing is Text) removeContent(messageId, dto.id) @@ -175,6 +188,7 @@ class SessionModel { @RequiresEdt fun appendDelta(messageId: String, contentId: String, delta: String) { val msg = entries[messageId] ?: return + if (hiddenText.contains(messageId to contentId)) return val existing = msg.parts[contentId] val created = existing == null if (existing != null) { @@ -238,6 +252,7 @@ class SessionModel { @RequiresEdt fun loadHistory(history: List) { entries.clear() + hiddenText.clear() session = null state = SessionState.Idle diff = emptyList() @@ -247,6 +262,10 @@ class SessionModel { val item = Message(msg.info) for (part in msg.parts) { if (part.type in SILENT_PART_TYPES) continue + if (hiddenSynthetic(item, part)) { + hiddenText.add(msg.info.id to part.id) + continue + } if (empty(part)) continue val content = fromDto(part, part.text) item.parts[content.id] = content @@ -262,6 +281,7 @@ class SessionModel { fun clear() { entries.clear() turnEntries.clear() + hiddenText.clear() session = null state = SessionState.Idle diff = emptyList() @@ -382,6 +402,7 @@ class SessionModel { existing.mime = dto.mime ?: "application/octet-stream" existing.url = dto.url ?: "" existing.filename = dto.filename + existing.source = dto.source } is Tool -> { existing.kind = toolKind(dto.tool) @@ -410,6 +431,9 @@ class SessionModel { private fun empty(dto: PartDto) = dto.type == "text" && dto.text?.isNotBlank() != true + private fun hiddenSynthetic(msg: Message, dto: PartDto) = + msg.info.role == "user" && dto.type == "text" && dto.synthetic == true + private fun fromDto(dto: PartDto, text: CharSequence? = null): Content { val content = text ?: dto.text return when (dto.type) { @@ -424,6 +448,7 @@ class SessionModel { mime = dto.mime ?: "application/octet-stream" url = dto.url ?: "" filename = dto.filename + source = dto.source } "tool" -> Tool(dto.id, dto.tool ?: "unknown", toolKind(dto.tool)).apply { state = parseToolState(dto.state) @@ -583,9 +608,11 @@ data class ModelItem( val providerName: String, val recommendedIndex: Double?, val free: Boolean, + val byok: Boolean = false, val variants: List, val limit: ModelLimitItem?, val attachment: Boolean = false, + val mayTrainOnYourPrompts: Boolean = false, ) { val key: String get() = "$provider/$id" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt index 7d50410baeb..b3db2c7d410 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/ReasoningPicker.kt @@ -80,6 +80,11 @@ class ReasoningPicker : PickerButton() { cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } + fun open() { + if (!isEnabled || items.isEmpty()) return + showPopup() + } + private fun showPopup() { val step = object : BaseListPopupStep("", items) { override fun getTextFor(value: Item) = value.display diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/editor/SessionEditorTextField.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/editor/SessionEditorTextField.kt index 4b9800b76ba..f42a02439ab 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/editor/SessionEditorTextField.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/editor/SessionEditorTextField.kt @@ -1,12 +1,27 @@ package ai.kilocode.client.session.ui.editor +import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.prompt.PromptDataKeys import ai.kilocode.client.session.ui.prompt.SendPromptContext import ai.kilocode.client.session.ui.selection.SessionSelection +import com.intellij.ide.actions.UndoRedoAction +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.actionSystem.PlatformCoreDataKeys +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.ui.EditorTextField +import com.intellij.ui.LanguageTextField +import com.intellij.util.textCompletion.TextCompletionProvider +import com.intellij.util.textCompletion.TextCompletionUtil /** * A session-scoped [EditorTextField] for plain-text input. @@ -23,13 +38,74 @@ import com.intellij.ui.EditorTextField * wrapping here. */ internal open class SessionEditorTextField( - project: Project, + private val project: Project, private val ctx: SendPromptContext? = null, + completion: TextCompletionProvider? = null, private val selection: SessionSelection? = null, -) : EditorTextField(project, PlainTextFileType.INSTANCE) { +) : EditorTextField( + completion?.let { + LanguageTextField.createDocument( + "", + PlainTextLanguage.INSTANCE, + project, + TextCompletionUtil.DocumentWithCompletionCreator(it, true), + ) + }, + project, + PlainTextFileType.INSTANCE, +) { + private val undo = action(KiloBundle.message("session.editor.undo"), true) + private val redo = action(KiloBundle.message("session.editor.redo"), false) + + init { + addSettingsProvider(::install) + } + override fun uiDataSnapshot(sink: DataSink) { super.uiDataSnapshot(sink) selection?.provideCopy(sink) { text } ctx?.let { sink.set(PromptDataKeys.SEND, it) } + file()?.let { sink.set(PlatformCoreDataKeys.FILE_EDITOR, it) } + } + + private fun install(editor: Editor) { + editor.contentComponent.putClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER, true) + // Workaround: global $Undo/$Redo can miss the synthetic FileEditor for this embedded + // EditorTextField. Bind the shortcuts locally until the platform data context targets it reliably. + val manager = ActionManager.getInstance() + manager.getAction(IdeActions.ACTION_UNDO)?.shortcutSet?.let { + undo.registerCustomShortcutSet(it, editor.contentComponent) + } + manager.getAction(IdeActions.ACTION_REDO)?.shortcutSet?.let { + redo.registerCustomShortcutSet(it, editor.contentComponent) + } + } + + private fun action(text: String, undo: Boolean) = object : DumbAwareAction(text) { + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = available(undo) + } + + override fun actionPerformed(e: AnActionEvent) { + if (project.isDisposed) return + val file = file() ?: return + val manager = UndoManager.getInstance(project) + if (undo) { + if (manager.isUndoAvailable(file)) manager.undo(file) + return + } + if (manager.isRedoAvailable(file)) manager.redo(file) + } + } + + private fun available(undo: Boolean): Boolean { + if (project.isDisposed) return false + val file = file() ?: return false + val manager = UndoManager.getInstance(project) + return if (undo) manager.isUndoAvailable(file) else manager.isRedoAvailable(file) + } + + private fun file(): TextEditor? { + return getEditor(false)?.let(TextEditorProvider.getInstance()::getTextEditor) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt index 6c9a03b6565..c3381ec4d41 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/mode/ModePicker.kt @@ -66,6 +66,11 @@ class ModePicker : PickerButton() { cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } + fun open() { + if (!isEnabled || items.isEmpty()) return + showPopup() + } + private fun showPopup() { val item = selected ?: items.first() val popup = JBPopupFactory.getInstance() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index a7b8efe4ddf..6ae1c2bec11 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -56,8 +56,10 @@ class ModelPicker : PickerButton() { val providerName: String, val recommendedIndex: Double? = null, val free: Boolean = false, + val byok: Boolean = false, val variants: List = emptyList(), val attachment: Boolean = false, + val mayTrainOnYourPrompts: Boolean = false, ) { val key: String get() = "$provider/$id" @@ -135,6 +137,11 @@ class ModelPicker : PickerButton() { cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } + fun open() { + if (!isEnabled || (items.isEmpty() && !allowEmpty)) return + showPopup() + } + private fun showPopup() { val rows = modelPickerRows(items, favorites(), "", allowEmpty, emptyText, includeSmall) val model = CollectionListModel(rows) @@ -473,7 +480,7 @@ internal object ModelText { fun freeLabel(): String = KiloBundle.message("model.picker.free") - fun collectsData(item: ModelPicker.Item): Boolean = item.free && item.provider == "kilo" + fun collectsData(item: ModelPicker.Item): Boolean = item.mayTrainOnYourPrompts fun freeBg(): JBColor = JBColor.namedColor("Kilo.ModelPicker.freeBadgeBackground", JBColor(0x95D6AC, 0x7FCA99)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 4ce73c10a49..e3e9797dd22 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -74,6 +74,14 @@ internal class ModelPickerRenderer( private val badgeLabel = BadgeLabel(badge).apply { border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) } + private val byok = FilledBadgeIcon( + "BYOK", + UiStyle.Colors.badgeBg(), + UiStyle.Colors.badgeFg(), + ) + private val byokLabel = BadgeLabel(byok).apply { + border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) + } private val warn = JBLabel(DATA_COLLECTED).apply { toolTipText = ModelText.dataCollected() border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) @@ -83,6 +91,7 @@ internal class ModelPickerRenderer( add(title) add(warn) add(badgeLabel) + add(byokLabel) add(provider) } private val star = JBLabel().apply { @@ -136,6 +145,7 @@ internal class ModelPickerRenderer( if (item == null) { title.append(value.emptyText, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) badgeLabel.isVisible = false + byokLabel.isVisible = false warn.isVisible = false provider.isVisible = false star.icon = EmptyIcon.ICON_16 @@ -150,7 +160,8 @@ internal class ModelPickerRenderer( title.append(name.model, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) warn.isVisible = ModelText.collectsData(item) - badgeLabel.isVisible = item.free + badgeLabel.isVisible = item.free && !item.byok + byokLabel.isVisible = item.byok provider.isVisible = value.favorite provider.text = item.providerName provider.foreground = weak @@ -174,6 +185,8 @@ internal class ModelPickerRenderer( internal fun badgeText(): String = badge.text + internal fun byokVisible(): Boolean = byokLabel.isVisible + internal fun warningVisible(): Boolean = warn.isVisible internal fun warningTooltip(): String? = warn.toolTipText diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt new file mode 100644 index 00000000000..5e40551cd03 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt @@ -0,0 +1,306 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.app.Workspace +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.rpc.dto.CommandDto +import ai.kilocode.rpc.dto.FileSearchResultDto +import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.completion.PlainPrefixMatcher +import com.intellij.codeInsight.completion.PrioritizedLookupElement +import com.intellij.codeInsight.lookup.AutoCompletionPolicy +import com.intellij.codeInsight.lookup.CharFilter +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.util.textCompletion.TextCompletionProvider +import java.util.Collections +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class KiloPromptCompletionProvider( + private val workspace: Workspace, + private val service: KiloWorkspaceService, + private val actions: List, + private val mentions: List, + private val scope: CoroutineScope, +) : TextCompletionProvider, DumbAware { + private val paths = Collections.synchronizedSet(mutableSetOf()) + private val exists = Collections.synchronizedMap(mutableMapOf()) + private val pending = Collections.synchronizedSet(mutableSetOf()) + private val cache: MutableMap = Collections.synchronizedMap( + object : LinkedHashMap(64, 0.75f, true) { + override fun removeEldestEntry(eldest: Map.Entry) = size > 64 + }, + ) + + data class Highlight(val start: Int, val end: Int, val kind: HighlightKind) + + enum class HighlightKind { MENTION, COMMAND, INVALID } + + /** A file mention token at a caret/mouse offset, with whether it resolves to a real file. */ + data class MentionHit(val start: Int, val end: Int, val value: String, val resolved: Boolean) + + /** + * The file mention spanning [offset], or null when the offset is outside a mention, + * on a special (`@git-changes`) token, or on a mention not yet validated. + */ + fun mentionAt(text: String, offset: Int): MentionHit? { + val span = mentionSpans(text).firstOrNull { offset in it.start..it.end } ?: return null + if (span.value in mentionNames()) return null + val resolved = span.value in paths || exists[span.value] == true + if (!resolved && exists[span.value] != false) return null + return MentionHit(span.start, span.end, span.value, resolved) + } + + /** Open a resolved file mention using the workspace's go-to-file plumbing. */ + fun navigate(value: String) { + scope.launch { service.openPath(workspace.directory, value) } + } + + fun clearMentions() { + paths.clear() + exists.clear() + pending.clear() + cache.clear() + } + + fun prewarm() { + if (cache.containsKey("")) return + scope.launch { + val result = service.searchFiles(workspace.directory, "", 50) + if (result.files.isNotEmpty() || result.git) cache.putIfAbsent("", result) + } + } + + fun inside(text: String, caret: Int): Boolean = mentionSpans(text).any { span -> caret in span.start..span.end } + + private fun clientTokens(): Set = actions.flatMapTo(mutableSetOf()) { action -> + listOf(action.name) + action.hints + } + + fun clientAction(text: String): SlashAction? { + val name = commandName(text) ?: return null + return actions.firstOrNull { action -> name == action.name || name in action.hints } + } + + fun mentionNames(): Set = mentions.mapTo(mutableSetOf()) { it.name } + + fun serverCommand(text: String): Pair? { + val name = commandName(text) ?: return null + if (actions.any { action -> name == action.name || name in action.hints }) return null + if (workspace.state.value.commands.none { it.name == name }) return null + val raw = text.trimStart() + return name to raw.drop(name.length + 1).trimStart() + } + + fun highlights(text: String, caret: Int = -1): List = buildList { + val command = text.takeIf { it.startsWith('/') } + ?.drop(1) + ?.takeWhile { !it.isWhitespace() } + ?.takeIf { it.isNotBlank() } + val commands = workspace.state.value.commands.mapTo(mutableSetOf()) { it.name } + if (command != null && (clientAction("/$command") != null || command in commands)) { + add(Highlight(0, command.length + 1, HighlightKind.COMMAND)) + } + + mentionSpans(text).forEach { span -> + val under = caret in span.start..span.end + when { + span.value in mentionNames() -> add(Highlight(span.start, span.end, HighlightKind.MENTION)) + span.value in paths || exists[span.value] == true -> add(Highlight(span.start, span.end, HighlightKind.MENTION)) + under -> Unit + exists[span.value] == false -> add(Highlight(span.start, span.end, HighlightKind.INVALID)) + } + } + } + + fun validate(text: String, caret: Int, onResolved: () -> Unit) { + mentionSpans(text).forEach { span -> + val value = span.value + if (value in mentionNames()) return@forEach + if (value in paths) return@forEach + if (caret in span.start..span.end) return@forEach + if (exists.containsKey(value)) return@forEach + if (!pending.add(value)) return@forEach + scope.launch { + val ok = runCatching { service.files(workspace.directory, value).isNotEmpty() }.getOrDefault(false) + exists[value] = ok + pending.remove(value) + onResolved() + } + } + } + + override fun getAdvertisement(): String? = null + + override fun getPrefix(text: String, offset: Int): String? = token(text, offset)?.prefix + + override fun applyPrefixMatcher(result: CompletionResultSet, prefix: String): CompletionResultSet = + result.withPrefixMatcher(PlainPrefixMatcher(prefix)).caseInsensitive() + + override fun acceptChar(c: Char): CharFilter.Result = when { + c.isWhitespace() -> CharFilter.Result.HIDE_LOOKUP + else -> CharFilter.Result.ADD_TO_PREFIX + } + + override fun fillCompletionVariants(parameters: CompletionParameters, prefix: String, result: CompletionResultSet) { + when (token(parameters.originalFile.text, parameters.offset)?.kind) { + Kind.SLASH -> slash(prefix, result) + Kind.MENTION -> mention(prefix, result) + null -> Unit + } + result.stopHere() + } + + private fun slash(prefix: String, result: CompletionResultSet) { + result.restartCompletionOnAnyPrefixChange() + val out = result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) + val names = clientTokens() + val clients = actions.filter { action -> matches(prefix, action.name, action.hints) } + clients.forEach { action -> out.addElement(client(action)) } + val commands = workspace.state.value.commands + .filter { it.name !in names && matches(prefix, it.name, it.hints) } + commands.forEach { command -> out.addElement(server(command)) } + if (clients.isNotEmpty() || commands.isNotEmpty()) return + result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) + .addElement(info(prefix, KiloBundle.message("prompt.completion.noMatches"))) + } + + private fun mention(prefix: String, result: CompletionResultSet) { + result.restartCompletionOnAnyPrefixChange() + val out = result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) + val search = search(prefix) + val known = mentions.filter { action -> matches(prefix, action.name, action.hints) && action.available(search) } + known.forEach { action -> out.addElement(prioritize(resource(action))) } + if (search.indexing) { + val msg = KiloBundle.message("prompt.mention.indexing") + result.addLookupAdvertisement(msg) + out.addElement(info(prefix, msg)) + return + } + search.files.forEach { file -> out.addElement(file(file)) } + if (known.isEmpty() && search.files.isEmpty()) { + val msg = KiloBundle.message("prompt.completion.noMatches") + out.addElement(info(prefix, msg)) + } + } + + private fun search(prefix: String): FileSearchResultDto = cache[prefix] ?: fetch(prefix) + + private fun fetch(prefix: String): FileSearchResultDto { + val result = runBlockingCancellable { service.searchFiles(workspace.directory, prefix, 50) } + cache[prefix] = result + return result + } + + private fun info(prefix: String, msg: String): LookupElement = LookupElementBuilder.create(msg) + .withPresentableText(msg) + .withIcon(AllIcons.General.Information) + .withInsertHandler { ctx, _ -> + val start = (ctx.startOffset - prefix.length).coerceAtLeast(0) + val tail = ctx.tailOffset.coerceAtMost(ctx.document.textLength) + val end = (tail until ctx.document.textLength).firstOrNull { ctx.document.text[it].isWhitespace() } + ?: ctx.document.textLength + ctx.document.replaceString(start, end, prefix) + ctx.editor.caretModel.moveToOffset(start + prefix.length) + } + .withAutoCompletionPolicy(AutoCompletionPolicy.NEVER_AUTOCOMPLETE) + + private fun matches(prefix: String, name: String, hints: List): Boolean = + (listOf(name) + hints).any { it.startsWith(prefix, ignoreCase = true) } + + private fun commandName(text: String): String? { + val raw = text.trimStart() + if (!raw.startsWith('/')) return null + return raw.drop(1).takeWhile { !it.isWhitespace() }.takeIf { it.isNotBlank() } + } + + private fun client(action: SlashAction): LookupElement = LookupElementBuilder.create(action.name) + .withPresentableText("/${action.name}") + .withTailText(" ${action.description}", true) + .withLookupStrings(action.hints) + .withIcon(AllIcons.Actions.Execute) + .withInsertHandler { ctx, _ -> + ctx.document.setText("") + ApplicationManager.getApplication().invokeLater { action.action() } + } + + private fun server(command: CommandDto): LookupElement = LookupElementBuilder.create(command.name) + .withPresentableText("/${command.name}") + .withTailText(command.description?.let { " $it" } ?: "", true) + .withTypeText(command.source) + .withLookupStrings(command.hints) + .withIcon(AllIcons.Nodes.Function) + .withInsertHandler { ctx, _ -> replace(ctx, "/${command.name} ", false) } + + private fun resource(action: MentionAction): LookupElement = LookupElementBuilder.create(action.name) + .withPresentableText("@${action.name}") + .withTailText(" ${action.description}", true) + .withLookupStrings(action.hints) + .withIcon(AllIcons.Nodes.Tag) + .withInsertHandler { ctx, _ -> replace(ctx, "@${action.name} ", false) } + + private fun prioritize(element: LookupElement): LookupElement = + PrioritizedLookupElement.withGrouping(PrioritizedLookupElement.withPriority(element, 100.0), 100) + + private fun file(file: WorkspaceFileDto): LookupElement = LookupElementBuilder.create(file.path) + .withPresentableText("@${file.path}") + .withTailText(parent(file.path), true) + .withIcon(icon(file)) + .withLookupString(file.name) + .withInsertHandler { ctx, _ -> replace(ctx, "@${file.path} ", true, file.path) } + + private fun icon(file: WorkspaceFileDto) = when { + file.directory -> AllIcons.Nodes.Folder + else -> FileTypeManager.getInstance().getFileTypeByFileName(file.name).icon ?: AllIcons.FileTypes.Text + } + + private fun replace(ctx: InsertionContext, value: String, trim: Boolean, path: String? = null) { + val text = ctx.document.text + val offset = ctx.startOffset.coerceAtMost(text.length) + val start = (offset - 1 downTo 0).firstOrNull { text[it].isWhitespace() }?.plus(1) ?: 0 + val end = tokenEnd(text, start) + val next = text.getOrNull(end) + val insert = if (trim && next?.isWhitespace() == true) value.trimEnd() else value + path?.let { + paths.add(it) + exists[it] = true + } + ctx.document.replaceString(start, end, insert) + ctx.editor.caretModel.moveToOffset(start + insert.length) + } + + private fun tokenEnd(text: String, start: Int): Int = + (start until text.length).firstOrNull { text[it].isWhitespace() } ?: text.length + + private fun parent(path: String): String { + val idx = path.lastIndexOf('/') + if (idx <= 0) return "" + return " ${path.substring(0, idx)}" + } + + private fun token(text: String, offset: Int): Token? { + val pos = offset.coerceIn(0, text.length) + val start = (pos - 1 downTo 0).firstOrNull { text[it].isWhitespace() }?.plus(1) ?: 0 + val end = (pos until text.length).firstOrNull { text[it].isWhitespace() } ?: text.length + val head = text.substring(start, pos) + val raw = text.substring(start, end) + if (raw.startsWith("/") && text.take(start).isBlank() && raw.indexOf(' ') < 0) return Token(Kind.SLASH, head.drop(1)) + if (raw.startsWith("@") && raw.indexOf(' ') < 0) return Token(Kind.MENTION, head.drop(1)) + return null + } + + private data class Token(val kind: Kind, val prefix: String) + + private fun mentionSpans(text: String): List = promptMentions(text) + + private enum class Kind { SLASH, MENTION } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionAction.kt new file mode 100644 index 00000000000..308b0f846c6 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionAction.kt @@ -0,0 +1,33 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.rpc.dto.FileSearchResultDto + +data class MentionAction( + val name: String, + val description: String, + val hints: List = emptyList(), + val available: (FileSearchResultDto) -> Boolean, +) { + data class Spec( + val name: String, + val descriptionKey: String, + val hints: List = emptyList(), + val filename: String, + val uri: String, + val available: (FileSearchResultDto) -> Boolean, + ) { + val token: String get() = "@$name" + } + + companion object { + val GIT_CHANGES = Spec( + "git-changes", + "prompt.mention.gitChanges", + filename = "git-changes.txt", + uri = "git-changes", + available = { it.git }, + ) + + val ALL = listOf(GIT_CHANGES) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigator.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigator.kt new file mode 100644 index 00000000000..5efe9a872f8 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigator.kt @@ -0,0 +1,103 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.event.EditorMouseEvent +import com.intellij.openapi.editor.event.EditorMouseListener +import com.intellij.openapi.editor.event.EditorMouseMotionListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.util.SystemInfo +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Cursor +import java.awt.event.MouseEvent + +/** + * Makes resolved `@file` mentions behave like the platform's "Go to File": + * + * - Holding the navigation modifier (Cmd on macOS, Ctrl elsewhere) and hovering a resolved + * mention paints it as a hyperlink and shows the hand cursor; clicking opens the file. + * - Go to Declaration (Cmd-B / Ctrl-B) opens the mention under the caret. + * - Hovering an unresolved (red) mention shows a tooltip explaining why it cannot be resolved. + * + * Implemented locally on the frontend editor — like completion and undo here — because the + * platform navigation machinery targets the backend-shared editor in split mode. + */ +internal class MentionNavigator( + private val editor: EditorEx, + private val provider: KiloPromptCompletionProvider, +) : EditorMouseListener, EditorMouseMotionListener { + + private var link: RangeHighlighter? = null + + @RequiresEdt + fun install() { + editor.addEditorMouseListener(this) + editor.addEditorMouseMotionListener(this) + val base = ActionManager.getInstance().getAction(IdeActions.ACTION_GOTO_DECLARATION) ?: return + object : DumbAwareAction(KiloBundle.message("prompt.mention.goto")) { + override fun actionPerformed(e: AnActionEvent) { + val hit = provider.mentionAt(editor.document.text, editor.caretModel.offset) ?: return + if (hit.resolved) provider.navigate(hit.value) + } + }.registerCustomShortcutSet(base.shortcutSet, editor.contentComponent) + } + + override fun mouseMoved(e: EditorMouseEvent) { + val hit = if (e.isOverText) provider.mentionAt(editor.document.text, e.offset) else null + syncTooltip(hit) + syncLink(hit, e.mouseEvent) + } + + override fun mouseClicked(e: EditorMouseEvent) { + if (!modifier(e.mouseEvent) || !e.isOverText) return + val hit = provider.mentionAt(editor.document.text, e.offset) ?: return + if (!hit.resolved) return + e.consume() + provider.navigate(hit.value) + } + + override fun mouseExited(e: EditorMouseEvent) { + syncTooltip(null) + clearLink() + } + + private fun syncTooltip(hit: KiloPromptCompletionProvider.MentionHit?) { + val text = hit?.takeIf { !it.resolved }?.let { KiloBundle.message("prompt.mention.unresolved", it.value) } + if (editor.contentComponent.toolTipText != text) editor.contentComponent.toolTipText = text + } + + private fun syncLink(hit: KiloPromptCompletionProvider.MentionHit?, mouse: MouseEvent) { + val active = hit?.takeIf { it.resolved && modifier(mouse) } + if (active == null) { + clearLink() + return + } + if (link?.startOffset == active.start && link?.endOffset == active.end) return + clearLink() + val attributes = EditorColorsManager.getInstance().globalScheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR) + link = editor.markupModel.addRangeHighlighter( + active.start, + active.end, + HighlighterLayer.HYPERLINK, + attributes, + HighlighterTargetArea.EXACT_RANGE, + ) + editor.setCustomCursor(MentionNavigator::class.java, Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) + } + + private fun clearLink() { + link?.let(editor.markupModel::removeHighlighter) + link = null + editor.setCustomCursor(MentionNavigator::class.java, null) + } + + private fun modifier(e: MouseEvent) = if (SystemInfo.isMac) e.isMetaDown else e.isControlDown +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptEditorTextField.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptEditorTextField.kt index 3686e36be16..b6db14ab92f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptEditorTextField.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptEditorTextField.kt @@ -3,9 +3,11 @@ package ai.kilocode.client.session.ui.prompt import ai.kilocode.client.session.ui.editor.SessionEditorTextField import ai.kilocode.client.session.ui.selection.SessionSelection import com.intellij.openapi.project.Project +import com.intellij.util.textCompletion.TextCompletionProvider internal class PromptEditorTextField( project: Project, ctx: SendPromptContext, + completion: TextCompletionProvider?, selection: SessionSelection? = null, -) : SessionEditorTextField(project, ctx, selection) +) : SessionEditorTextField(project, ctx, completion, selection) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionParts.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionParts.kt new file mode 100644 index 00000000000..3fbddc5b497 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionParts.kt @@ -0,0 +1,89 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto +import ai.kilocode.rpc.dto.PromptPartDto +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +data class Mention(val value: String, val start: Int, val end: Int) + +fun promptMentions(text: String): List = buildList { + var pos = 0 + while (pos < text.length) { + val start = (pos until text.length).firstOrNull { !text[it].isWhitespace() } ?: return@buildList + val end = tokenEnd(text, start) + if (text[start] == '@' && end > start + 1) add(Mention(text.substring(start + 1, end), start, end)) + pos = end + 1 + } +} + +fun fileMentions(text: String, reserved: Set): List { + val seen = mutableSetOf() + return promptMentions(text).filter { item -> item.value !in reserved && seen.add(item.value) } +} + +suspend fun mentionParts( + text: String, + directory: String, + reserved: Set, + resolve: suspend (String) -> Boolean, + gitChanges: suspend () -> String?, +): List = coroutineScope { + val files = fileMentions(text, reserved) + val paths = files.map { item -> async { item.value to resolve(item.value) } } + .mapNotNullTo(mutableSetOf()) { item -> item.await().takeIf { it.second }?.first } + buildList { + addAll(mentionFileParts(text, paths, directory)) + if (text.contains(MentionAction.GIT_CHANGES.token)) gitChangesPart(text, gitChanges())?.let(::add) + } +} + +fun mentionFileParts(text: String, paths: Set, directory: String): List = buildList { + fileMentions(text, emptySet()).filter { item -> item.value in paths }.forEach { mention -> + val target = runCatching { + val item = Path.of(mention.value) + if (item.isAbsolute) item else Path.of(directory).resolve(item).normalize() + }.getOrNull() ?: return@forEach + add(PromptPartDto( + type = "file", + mime = "text/plain", + url = target.toUri().toString(), + filename = target.fileName?.toString(), + source = source("file", "@${mention.value}", mention.start, path = mention.value), + )) + } +} + +fun gitChangesPart(text: String, diff: String?): PromptPartDto? { + val spec = MentionAction.GIT_CHANGES + val start = text.mentionStart(spec.token) ?: return null + val value = diff?.takeIf { it.isNotBlank() } ?: return null + return dataPart(spec.filename, value, source("file", spec.token, start, path = spec.uri)) +} + +private fun String.mentionStart(token: String): Int? = promptMentions(this) + .firstOrNull { item -> "@${item.value}" == token } + ?.start + +private fun tokenEnd(text: String, start: Int): Int = + (start until text.length).firstOrNull { text[it].isWhitespace() } ?: text.length + +private fun dataPart(name: String, text: String, source: PartSourceDto? = null): PromptPartDto { + val data = URLEncoder.encode(text, StandardCharsets.UTF_8).replace("+", "%20") + return PromptPartDto(type = "file", mime = "text/plain", url = "data:text/plain;charset=utf-8,$data", filename = name, source = source) +} + +private fun source( + type: String, + token: String, + start: Int, + path: String? = null, +) = PartSourceDto( + type = type, + text = PartSourceTextDto(value = token, start = start.toDouble(), end = (start + token.length).toDouble()), + path = path, +) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index 633ce6ccfb9..676c265fca7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -21,6 +21,12 @@ import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import ai.kilocode.rpc.dto.PromptPartDto import com.intellij.icons.AllIcons +import com.intellij.codeInsight.completion.CodeCompletionHandlerBase +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.lookup.LookupEx +import com.intellij.codeInsight.lookup.LookupManagerListener +import com.intellij.codeInsight.lookup.LookupPositionStrategy +import com.intellij.codeInsight.lookup.LookupPresentation import com.intellij.ide.DataManager import com.intellij.ide.dnd.DnDEvent import com.intellij.ide.dnd.DnDSupport @@ -28,18 +34,28 @@ import com.intellij.ide.dnd.FileCopyPasteUtil import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataSink import com.intellij.openapi.actionSystem.UiDataProvider import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.colors.CodeInsightColors +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea +import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.keymap.Keymap import com.intellij.openapi.keymap.KeymapManagerListener import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.util.IconLoader import com.intellij.ui.AnimatedIcon import com.intellij.ui.JBColor @@ -82,6 +98,8 @@ class PromptPanel( private val onSend: (String, List) -> Unit, private val onAbort: () -> Unit, private val onEnhance: (String, (Result) -> Unit) -> Unit, + private val onMentions: (String) -> List = { emptyList() }, + private val completion: KiloPromptCompletionProvider? = null, private val selection: SessionSelection? = null, ) : BorderLayoutPanel(), SessionEditorStyleTarget, SendPromptContext, UiDataProvider { @@ -92,6 +110,9 @@ class PromptPanel( private val SHIELD_ICON: Icon = IconLoader.getIcon("/icons/shield.svg", PromptPanel::class.java) private val SHIELD_FILLED_ICON: Icon = IconLoader.getIcon("/icons/shield-filled.svg", PromptPanel::class.java) private val WAND_ICON: Icon = IconLoader.getIcon("/icons/wand-sparkles.svg", PromptPanel::class.java) + private val MENTION_KEY = DefaultLanguageHighlighterColors.METADATA + private val COMMAND_KEY = DefaultLanguageHighlighterColors.KEYWORD + private val INVALID_KEY = CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES } val mode = ModePicker() @@ -114,8 +135,13 @@ class PromptPanel( ) } private val attachments = mutableListOf() + private val highlighters = mutableListOf() private val strip = PromptAttachmentStrip(project) { removeAttachment(it) } private var bus: MessageBusConnection? = null + private var lookupBus: MessageBusConnection? = null + private var completionAction: AnAction? = null + private var completionTarget: JComponent? = null + private var mentionCaret = false private var autoApprove = false private var attachment = true private var submitting = false @@ -126,7 +152,7 @@ class PromptPanel( } } - private val editor = PromptEditorTextField(project, this, selection).apply { + private val editor = PromptEditorTextField(project, this, completion, selection).apply { border = JBUI.Borders.empty() setFontInheritedFromLAF(false) setPlaceholder(placeholder()) @@ -148,8 +174,20 @@ class PromptPanel( ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + installCompletionShortcut(ed) + completion?.let { MentionNavigator(ed, it).install() } installFileDrop(ed.contentComponent, "editor") installFileDrop(ed.scrollPane, "scroll") + syncHighlights() + ed.caretModel.addCaretListener(object : CaretListener { + override fun caretPositionChanged(e: CaretEvent) { + val provider = completion ?: return + val inside = provider.inside(ed.document.text, ed.caretModel.offset) + if (mentionCaret == inside) return + mentionCaret = inside + syncHighlights() + } + }) ed.contentComponent.addFocusListener(object : FocusAdapter() { override fun focusGained(e: FocusEvent) { repaint() @@ -218,6 +256,8 @@ class PromptPanel( override fun documentChanged(e: DocumentEvent) { invalidateEnhancement() syncEditorHeight() + triggerCompletion(e) + syncHighlights() onChange() } }) @@ -262,6 +302,7 @@ class PromptPanel( @RequiresEdt fun setReady(value: Boolean) { ready = value + if (value) completion?.prewarm() if (!value) invalidateEnhancement() else syncEnhance() } @@ -334,14 +375,58 @@ class PromptPanel( editor.background = style.editorScheme.defaultBackground syncEditorHeight() syncAutoApprove() + syncHighlights() + } + + @RequiresEdt + fun refreshHighlights() { + syncHighlights() } @RequiresEdt fun clear() { editor.text = "" attachments.clear() + completion?.clearMentions() + completion?.prewarm() strip.clear() syncEditorHeight() + syncHighlights() + } + + @RequiresEdt + private fun syncHighlights() { + val provider = completion ?: return + val ed = editor.getEditor(false) ?: return + highlighters.forEach(ed.markupModel::removeHighlighter) + highlighters.clear() + val length = ed.document.textLength + val text = ed.document.text + val caret = ed.caretModel.offset + provider.validate(text, caret) { + ApplicationManager.getApplication().invokeLater { + if (!project.isDisposed) refreshHighlights() + } + } + provider.highlights(text, caret).forEach { item -> + val start = item.start.coerceIn(0, length) + val end = item.end.coerceIn(start, length) + if (start == end) return@forEach + highlighters += ed.markupModel.addRangeHighlighter( + key(item.kind), + start, + end, + HighlighterLayer.SYNTAX + 1, + HighlighterTargetArea.EXACT_RANGE, + ) + } + mentionCaret = provider.inside(text, caret) + } + + private fun key(kind: KiloPromptCompletionProvider.HighlightKind): TextAttributesKey = when (kind) { + KiloPromptCompletionProvider.HighlightKind.MENTION -> MENTION_KEY + KiloPromptCompletionProvider.HighlightKind.COMMAND -> COMMAND_KEY + KiloPromptCompletionProvider.HighlightKind.INVALID -> INVALID_KEY } @RequiresEdt @@ -360,6 +445,7 @@ class PromptPanel( super.addNotify() bindRoot() bindKeymap() + bindLookup() } override fun removeNotify() { @@ -367,6 +453,9 @@ class PromptPanel( root = null bus?.disconnect() bus = null + lookupBus?.disconnect() + lookupBus = null + uninstallCompletionShortcut() super.removeNotify() } @@ -432,11 +521,13 @@ class PromptPanel( ApplicationManager.getApplication().executeOnPooledThread { try { val files = items.map { it.part() } + val mentioned = onMentions(txt) ApplicationManager.getApplication().invokeLater { submitting = false if (project.isDisposed) return@invokeLater - LOG.debug { "${ChatLogSummary.prompt(promptDto(txt, files))} src=$src busy=$busy" } - onSend(txt, files) + val parts = files + mentioned + LOG.debug { "${ChatLogSummary.prompt(promptDto(txt, parts))} src=$src busy=$busy" } + onSend(txt, parts) } } catch (e: Exception) { ApplicationManager.getApplication().invokeLater { @@ -449,6 +540,61 @@ class PromptPanel( } } + private fun triggerCompletion(e: DocumentEvent) { + if (project.isDisposed) return + val value = e.newFragment.toString() + if (value.length != 1) return + val text = editor.text + val offset = e.offset + value.length + val popup = value == "@" || (value == "/" && text.take(offset).trim() == "/") + if (!popup) return + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + editor.getEditor(false)?.let(::showCompletion) + } + } + + @RequiresEdt + @Suppress("UnstableApiUsage") + private fun showCompletion(ed: com.intellij.openapi.editor.Editor) { + // Run completion synchronously and locally on this frontend-only editor. The public entry + // points (ACTION_CODE_COMPLETION, AutoPopupController.scheduleAutoPopup) route through + // split-mode RD/autopopup machinery that targets a backend-shared editor: in split mode they + // either dispatch to the backend (which has no editor and asserts) or get cancelled before the + // popup shows. CodeCompletionHandlerBase is public (not @ApiStatus.Internal) and is the only + // API that drives the completion engine directly against this local editor. + CodeCompletionHandlerBase.createHandler(CompletionType.BASIC, true, false, true) + .invokeCompletion(project, ed, 1) + } + + @RequiresEdt + private fun installCompletionShortcut(ed: com.intellij.openapi.editor.Editor) { + if (completion == null) return + val base = ActionManager.getInstance().getAction(IdeActions.ACTION_CODE_COMPLETION) ?: return + val action = completionAction ?: object : DumbAwareAction(KiloBundle.message("prompt.completion.action")) { + override fun actionPerformed(e: AnActionEvent) { + editor.getEditor(false)?.let(::showCompletion) + } + }.also { completionAction = it } + uninstallCompletionShortcut() + val target = ed.contentComponent + action.registerCustomShortcutSet(base.shortcutSet, target) + completionTarget = target + } + + @RequiresEdt + private fun refreshCompletionShortcut() { + val ed = editor.getEditor(false) ?: return + installCompletionShortcut(ed) + } + + @RequiresEdt + private fun uninstallCompletionShortcut() { + val target = completionTarget ?: return + completionAction?.unregisterCustomShortcutSet(target) + completionTarget = null + } + @RequiresEdt private fun addAttachment(item: PromptAttachment) { if (!attachment && PromptAttachmentExtractor.media(item.mime)) { @@ -566,6 +712,7 @@ class PromptPanel( override fun activeKeymapChanged(keymap: Keymap?) { editor.setPlaceholder(placeholder()) syncTooltip() + refreshCompletionShortcut() } override fun shortcutsChanged(keymap: Keymap, actionIds: Collection, fromSettings: Boolean) { @@ -574,10 +721,27 @@ class PromptPanel( editor.setPlaceholder(placeholder()) syncTooltip() } + if (IdeActions.ACTION_CODE_COMPLETION in actionIds) refreshCompletionShortcut() } }) } + @RequiresEdt + private fun bindLookup() { + if (lookupBus != null) return + val connection = project.messageBus.connect() + lookupBus = connection + connection.subscribe(LookupManagerListener.TOPIC, LookupManagerListener { _, next -> + // LookupPresentation/LookupPositionStrategy are @ApiStatus.Experimental (no stable API + // forces the lookup above the caret). LookupEx is the public lookup interface. + val lookup = next as? LookupEx ?: return@LookupManagerListener + if (lookup.editor !== editor.getEditor(false)) return@LookupManagerListener + lookup.presentation = LookupPresentation.Builder(lookup.presentation) + .withPositionStrategy(LookupPositionStrategy.ONLY_ABOVE) + .build() + }) + } + @RequiresEdt private fun syncTooltip() { button.toolTipText = tooltip() @@ -677,59 +841,7 @@ class PromptPanel( syncEditorHeight() } - private inner class SendButton : JButton(), UiDataProvider { - private var over = false - - init { - iconButton(this) - cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) - addMouseListener(object : MouseAdapter() { - override fun mouseEntered(e: MouseEvent) { - sync(true) - } - - override fun mouseExited(e: MouseEvent) { - sync(false) - } - }) - } - - override fun getPreferredSize() = JBUI.size( - SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE, - SessionUiStyle.View.Prompt.SEND_BUTTON_SIZE, - ) - - override fun uiDataSnapshot(sink: DataSink) { - sink.set(PromptDataKeys.SEND, this@PromptPanel) - } - - override fun getMinimumSize() = preferredSize - - override fun getMaximumSize() = preferredSize - - override fun paintComponent(g: Graphics) { - if (over) { - val g2 = g.create() as Graphics2D - try { - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) - g2.color = JBUI.CurrentTheme.ActionButton.hoverBackground() - val arc = JBUI.scale(JBUI.getInt("Button.arc", SessionUiStyle.View.Prompt.CORNER_ARC)) - g2.fillRoundRect(0, 0, width, height, arc, arc) - } finally { - g2.dispose() - } - } - super.paintComponent(g) - } - - private fun sync(value: Boolean) { - if (over == value) return - over = value - repaint() - } - } - - private inner class AutoApproveButton : JButton() { + private inner abstract class PromptButton : JButton() { private var over = false init { @@ -779,4 +891,12 @@ class PromptPanel( } } + private inner class SendButton : PromptButton(), UiDataProvider { + override fun uiDataSnapshot(sink: DataSink) { + sink.set(PromptDataKeys.SEND, this@PromptPanel) + } + } + + private inner class AutoApproveButton : PromptButton() + } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/SlashAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/SlashAction.kt new file mode 100644 index 00000000000..c64adaf30ee --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/SlashAction.kt @@ -0,0 +1,36 @@ +package ai.kilocode.client.session.ui.prompt + +data class SlashAction( + val name: String, + val description: String, + val hints: List = emptyList(), + val action: () -> Unit, +) { + data class Spec( + val name: String, + val descriptionKey: String, + val hints: List = emptyList(), + ) + + companion object { + val NEW = Spec("new", "prompt.slash.new", listOf("clear")) + val SESSIONS = Spec("sessions", "prompt.slash.sessions", listOf("history", "resume", "continue")) + val MODELS = Spec("models", "prompt.slash.models") + val AGENTS = Spec("agents", "prompt.slash.agents", listOf("modes")) + val VARIANT = Spec("variant", "prompt.slash.variant", listOf("reasoning", "variants", "thinking")) + val COMPACT = Spec("compact", "prompt.slash.compact", listOf("smol", "condense")) + val SETTINGS = Spec("settings", "prompt.slash.settings") + val HELP = Spec("help", "prompt.slash.help") + + val ALL = listOf( + NEW, + SESSIONS, + MODELS, + AGENTS, + VARIANT, + COMPACT, + SETTINGS, + HELP, + ) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index a9544308018..1335d1298c7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -114,11 +114,20 @@ class MessageView( fun upsertPart(content: Content) { if (content is StepFinish) return if (isHidden(content)) { + if (isPromptMention(content)) syncPromptMentions() // Remove any stale view for this content so it disappears when suppressed val id = aliases.remove(content.id) sources.remove(content.id) val stale = if (id == null) parts.remove(content.id) else null if (stale != null) { + if (stale is PromptAttachmentView) { + stale.remove(content.id) + if (!stale.isEmpty()) { + refresh() + return + } + attachments = null + } detach(stale) remove(stale) Disposer.dispose(stale) @@ -262,6 +271,7 @@ class MessageView( * pending/running question tool part linked to the active question. */ private fun isHidden(content: Content): Boolean { + if (isPromptMention(content)) return true if (content !is Tool) return false if (role == SessionUiStyle.View.Message.USER_ROLE && content.name == "read") return true if (content.name == "todoread") return true @@ -307,11 +317,24 @@ class MessageView( } private fun view(content: Content) = if (msg.info.role == SessionUiStyle.View.Message.USER_ROLE) { - ViewFactory.createUser(content, openFile, openUrl, selection, repo) { openAttachment(msg.info.id, it) } + ViewFactory.createUser(content, openFile, openUrl, selection, repo, promptMentions(msg)) { openAttachment(msg.info.id, it) } } else { ViewFactory.create(content, openFile, openUrl, selection, repo) { openAttachment(msg.info.id, it) } } + private fun syncPromptMentions() { + val mentions = promptMentions(msg) + for (view in parts.values) { + if (view is PromptView) view.setMentions(mentions) + } + } + + private fun isPromptMention(content: Content): Boolean { + if (role != SessionUiStyle.View.Message.USER_ROLE) return false + if (content !is FileAttachment) return false + return content.source != null && content.mime.lowercase().startsWith("text/plain") + } + /** Append a streaming delta to the renderer for [contentId]. */ @RequiresEdt fun appendDelta(contentId: String, delta: String): Boolean { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptMention.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptMention.kt new file mode 100644 index 00000000000..a9a3af505d7 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptMention.kt @@ -0,0 +1,71 @@ +package ai.kilocode.client.session.views + +import ai.kilocode.client.session.model.FileAttachment +import ai.kilocode.client.session.model.Message + +data class PromptMention( + val token: String, + val path: String, + val start: Int, + val end: Int, + val attachment: FileAttachment? = null, +) + +fun promptMentions(msg: Message): List = msg.parts.values.mapNotNull { part -> + if (part !is FileAttachment) return@mapNotNull null + if (!part.mime.lowercase().startsWith("text/plain")) return@mapNotNull null + val source = part.source ?: return@mapNotNull null + val path = source.path?.takeIf { it.isNotBlank() } ?: return@mapNotNull null + PromptMention( + token = source.text.value, + path = path, + start = source.text.start.toInt(), + end = source.text.end.toInt(), + attachment = part, + ) +} + +fun linkifyMentions(text: String, mentions: List): String { + if (mentions.isEmpty()) return text + val ranges = mutableListOf>() + val used = mutableListOf() + for (mention in mentions) { + val range = exact(text, mention)?.takeUnless { overlaps(it, used) } + ?: fallback(text, mention, used) + ?: continue + ranges.add(range to mention) + used.add(range) + } + val out = StringBuilder(text) + for ((range, mention) in ranges.sortedByDescending { it.first.first }) { + val link = link(mention) + out.replace(range.first, range.last + 1, link) + } + return out.toString() +} + +private fun exact(text: String, mention: PromptMention): IntRange? { + if (mention.start < 0 || mention.end > text.length || mention.start >= mention.end) return null + if (text.substring(mention.start, mention.end) != mention.token) return null + return mention.start until mention.end +} + +private fun fallback(text: String, mention: PromptMention, used: List): IntRange? { + if (mention.token.isEmpty()) return null + var at = text.indexOf(mention.token) + while (at >= 0) { + val range = at until at + mention.token.length + if (!overlaps(range, used)) return range + at = text.indexOf(mention.token, at + 1) + } + return null +} + +private fun overlaps(range: IntRange, used: List): Boolean = + used.any { range.first <= it.last && it.first <= range.last } + +private fun link(mention: PromptMention): String { + val text = mention.token.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]") + val href = mention.path.replace(" ", "%20").replace("(", "%28").replace(")", "%29") + return "[$text]($href)" +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptView.kt index c9c2a724617..958a8c6cb1d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PromptView.kt @@ -1,27 +1,83 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Text +import ai.kilocode.client.session.model.FileAttachment import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.session.model.Content +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.util.ui.JBUI class PromptView( text: Text, + private val openFile: (String) -> Unit = {}, + private val openAttachment: (FileAttachment) -> Unit = {}, openUrl: (String) -> Unit = {}, selection: SessionSelection? = null, + mentions: List = emptyList(), ) : TextView(text, transparent = true, openUrl = openUrl, selection = selection) { + private var mentions = mentions + private val buffer = StringBuilder(text.content) + init { border = JBUI.Borders.empty( JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), ) + sync() + } + + override fun update(content: Content) { + if (content !is Text) return + buffer.clear() + buffer.append(content.content) + sync() + } + + override fun appendDelta(delta: String) { + if (delta.isEmpty()) return + buffer.append(delta) + sync() + } + + fun setMentions(list: List) { + if (mentions == list) return + mentions = list + sync() + } + + override fun onLink(href: String) { + val mention = mentions.firstOrNull { it.path == href || path(it.path) == href } + if (mention != null) { + mention.attachment?.let { + openAttachment(it) + return + } + openFile(mention.path) + return + } + super.onLink(href) + } + + override fun applyStyle(style: SessionEditorStyle) { + super.applyStyle(style) + val color = style.editorScheme.getAttributes(DefaultLanguageHighlighterColors.METADATA)?.foregroundColor + if (color == null || md.linkColor == color) return + md.linkColor = color } override fun styleFont(style: SessionEditorStyle) = style.editorFont override fun styleBackground(style: SessionEditorStyle) = style.editorBackground + private fun sync() { + md.set(linkifyMentions(buffer.toString(), mentions)) + refresh() + } + + private fun path(value: String) = value.replace(" ", "%20").replace("(", "%28").replace(")", "%29") + override fun dumpLabel() = "PromptView#$contentId" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt index 6897e22af68..8eceececd97 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TextView.kt @@ -20,7 +20,7 @@ import javax.swing.JButton open class TextView( text: Text, transparent: Boolean = true, - openUrl: (String) -> Unit = {}, + private val openUrl: (String) -> Unit = {}, selection: SessionSelection? = null, ) : PartView() { @@ -35,7 +35,7 @@ open class TextView( isOpaque = false Disposer.register(this, md) md.opaque = !transparent - md.addLinkListener { openUrl(it.href) } + md.addLinkListener { onLink(it.href) } applyStyle(SessionEditorStyle.current()) add(md.component, BorderLayout.CENTER) add(toolbar, BorderLayout.SOUTH) @@ -78,8 +78,12 @@ open class TextView( /** Current markdown source — used by tests to assert rendered content. */ fun markdown(): String = md.markdown() + internal fun simulateLink(href: String) = md.simulateLink(href) + internal fun contentOpaque() = md.opaque + protected open fun onLink(href: String) = openUrl(href) + override fun applyStyle(style: SessionEditorStyle) { val font = styleFont(style) val bg = styleBackground(style) @@ -100,7 +104,7 @@ open class TextView( protected open fun styleBackground(style: SessionEditorStyle) = style.editorBackground - private fun refresh() { + protected fun refresh() { revalidate() repaint() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt index ba7a8c29dc0..871004d2c2e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt @@ -70,9 +70,10 @@ object ViewFactory { openUrl: (String) -> Unit = {}, selection: SessionSelection? = null, repo: String? = null, + mentions: List = emptyList(), openAttachment: (FileAttachment) -> Unit = { AttachmentView.openDefault(it, openFile, openUrl) }, ): PartView = when (content) { - is Text -> PromptView(content, openUrl = openUrl, selection = selection) + is Text -> PromptView(content, openFile = openFile, openAttachment = openAttachment, openUrl = openUrl, selection = selection, mentions = mentions) else -> create(content, openFile, openUrl, selection, repo, openAttachment) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt index 322e5fcfcbb..11dba93cde4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt @@ -174,7 +174,17 @@ internal class ModelsSettingsUi( .filter { it.id == KILO_PROVIDER || it.id in cfg.connected } .flatMap { provider -> provider.models.mapNotNull { (id, model) -> - val item = ModelPicker.Item(id, model.name, provider.id, provider.name, model.recommendedIndex, model.free, model.variants) + val item = ModelPicker.Item( + id, + model.name, + provider.id, + provider.name, + model.recommendedIndex, + model.free, + model.byok, + model.variants, + mayTrainOnYourPrompts = model.mayTrainOnYourPrompts, + ) if (!includeSmall && ModelText.small(item)) return@mapNotNull null item } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 59fa4586d93..0cf258b2850 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -136,10 +136,10 @@ session.header.context.used={0} / {1} tokens used session.header.context.reserved={0} reserved for output session.header.context.available={0} available -prompt.placeholder=Type a message... -prompt.placeholder.with.shortcuts=Type a message... ({0} to send, {1} for new line) -prompt.placeholder.with.send=Type a message... ({0} to send) -prompt.placeholder.with.newline=Type a message... ({0} for new line) +prompt.placeholder=Type here; use / or @, drop/paste files +prompt.placeholder.with.shortcuts=Type here; use / or @, drop/paste files; {0} send, {1} newline +prompt.placeholder.with.send=Type here; use / or @, drop/paste files; {0} send +prompt.placeholder.with.newline=Type here; use / or @, drop/paste files; {0} newline prompt.button.send=Send prompt.button.stop=Stop prompt.button.send.tooltip.stop=To stop, press {0} @@ -163,10 +163,26 @@ prompt.action.enhance.loading=Enhancing prompt... prompt.action.enhance.description=The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works. prompt.action.enhance.failed=Failed to enhance prompt prompt.action.enhance.failed.description=The configured small model could not enhance this prompt. +prompt.completion.action=Kilo Prompt Completion +prompt.completion.noMatches=No matches +prompt.mention.indexing=Indexing project files. File mentions will be available soon. +prompt.mention.gitChanges=Attach current git changes +prompt.mention.goto=Go to Mentioned File +prompt.mention.unresolved=Cannot resolve file "{0}" in the workspace +prompt.slash.new=Start a new session +prompt.slash.sessions=Open session history +prompt.slash.models=Choose a model +prompt.slash.agents=Choose an agent +prompt.slash.variant=Choose reasoning variant +prompt.slash.compact=Compact this session +prompt.slash.settings=Open Kilo settings +prompt.slash.help=Open Kilo documentation prompt.action.autoApprove.enable=Enable auto-approve prompt.action.autoApprove.disable=Disable auto-approve prompt.action.autoApprove.enabled.tooltip=Auto-approve is enabled. Permission prompts will be approved automatically. prompt.action.autoApprove.disabled.tooltip=Auto-approve is disabled. Click to approve permission prompts automatically. +session.editor.undo=Kilo Session Undo +session.editor.redo=Kilo Session Redo mode.picker.tooltip=Select mode mode.picker.deprecated=deprecated model.picker.tooltip=Select model diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index 39a29c4249a..0890f64c2cf 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -24,7 +24,7 @@ session.permission.title=طلب إذن session.permission.meta=الأداة: {0} • الأنماط: {1} session.permission.allow=سماح session.permission.deny=رفض -session.question.dismiss=رفض +session.question.dismiss=إغلاق session.status.considering=جار التفكير في الخطوات التالية… session.status.thinking=جار التفكير… @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} رمز مستخدم session.header.context.reserved={0} محجوز للإخراج session.header.context.available={0} متاح -prompt.placeholder=اكتب رسالة… -prompt.placeholder.with.shortcuts=اكتب رسالة… ({0} للإرسال، {1} لسطر جديد) -prompt.placeholder.with.send=اكتب رسالة… ({0} للإرسال) -prompt.placeholder.with.newline=اكتب رسالة… ({0} لسطر جديد) +prompt.placeholder=اكتب هنا؛ استخدم / أو @، وأسقط/الصق الملفات +prompt.placeholder.with.shortcuts=اكتب هنا؛ استخدم / أو @، وأسقط/الصق الملفات؛ {0} إرسال، {1} سطر جديد +prompt.placeholder.with.send=اكتب هنا؛ استخدم / أو @، وأسقط/الصق الملفات؛ {0} إرسال +prompt.placeholder.with.newline=اكتب هنا؛ استخدم / أو @، وأسقط/الصق الملفات؛ {0} سطر جديد prompt.button.send=إرسال prompt.button.stop=إيقاف prompt.button.send.tooltip.stop=للإيقاف، اضغط {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index 280b65d437c..f26bf14c4f9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokena korišteno session.header.context.reserved={0} rezervisano za izlaz session.header.context.available={0} dostupno -prompt.placeholder=Upišite poruku… -prompt.placeholder.with.shortcuts=Upišite poruku… ({0} za slanje, {1} za novi red) -prompt.placeholder.with.send=Upišite poruku… ({0} za slanje) -prompt.placeholder.with.newline=Upišite poruku… ({0} za novi red) +prompt.placeholder=Pišite ovdje; koristite / ili @, ispustite/zalijepite datoteke +prompt.placeholder.with.shortcuts=Pišite ovdje; koristite / ili @, ispustite/zalijepite datoteke; {0} pošalji, {1} novi red +prompt.placeholder.with.send=Pišite ovdje; koristite / ili @, ispustite/zalijepite datoteke; {0} pošalji +prompt.placeholder.with.newline=Pišite ovdje; koristite / ili @, ispustite/zalijepite datoteke; {0} novi red prompt.button.send=Pošalji prompt.button.stop=Zaustavi prompt.button.send.tooltip.stop=Da zaustavite, pritisnite {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index b8540bb70e8..001b10bad85 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens brugt session.header.context.reserved={0} reserveret til output session.header.context.available={0} tilgængelige -prompt.placeholder=Skriv en besked… -prompt.placeholder.with.shortcuts=Skriv en besked… ({0} for at sende, {1} for ny linje) -prompt.placeholder.with.send=Skriv en besked… ({0} for at sende) -prompt.placeholder.with.newline=Skriv en besked… ({0} for ny linje) +prompt.placeholder=Skriv her; brug / eller @, slip/indsæt filer +prompt.placeholder.with.shortcuts=Skriv her; brug / eller @, slip/indsæt filer; {0} send, {1} ny linje +prompt.placeholder.with.send=Skriv her; brug / eller @, slip/indsæt filer; {0} send +prompt.placeholder.with.newline=Skriv her; brug / eller @, slip/indsæt filer; {0} ny linje prompt.button.send=Send prompt.button.stop=Stop prompt.button.send.tooltip.stop=Tryk på {0} for at stoppe diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 9ff4d5868ef..2cab50026f4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} Token verwendet session.header.context.reserved={0} für Ausgabe reserviert session.header.context.available={0} verfügbar -prompt.placeholder=Nachricht eingeben… -prompt.placeholder.with.shortcuts=Nachricht eingeben… ({0} zum Senden, {1} für neue Zeile) -prompt.placeholder.with.send=Nachricht eingeben… ({0} zum Senden) -prompt.placeholder.with.newline=Nachricht eingeben… ({0} für neue Zeile) +prompt.placeholder=Hier tippen; / oder @ verwenden, Dateien ablegen/einfügen +prompt.placeholder.with.shortcuts=Hier tippen; / oder @ verwenden, Dateien ablegen/einfügen; {0} senden, {1} neue Zeile +prompt.placeholder.with.send=Hier tippen; / oder @ verwenden, Dateien ablegen/einfügen; {0} senden +prompt.placeholder.with.newline=Hier tippen; / oder @ verwenden, Dateien ablegen/einfügen; {0} neue Zeile prompt.button.send=Senden prompt.button.stop=Stopp prompt.button.send.tooltip.stop=Zum Stoppen drücken Sie {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 061266a1417..2eb7dbe49df 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens utilizados session.header.context.reserved={0} reservados para la salida session.header.context.available={0} disponibles -prompt.placeholder=Escribe un mensaje… -prompt.placeholder.with.shortcuts=Escribe un mensaje… ({0} para enviar, {1} para nueva línea) -prompt.placeholder.with.send=Escribe un mensaje… ({0} para enviar) -prompt.placeholder.with.newline=Escribe un mensaje… ({0} para nueva línea) +prompt.placeholder=Escribe aquí; usa / o @, suelta/pega archivos +prompt.placeholder.with.shortcuts=Escribe aquí; usa / o @, suelta/pega archivos; {0} enviar, {1} nueva línea +prompt.placeholder.with.send=Escribe aquí; usa / o @, suelta/pega archivos; {0} enviar +prompt.placeholder.with.newline=Escribe aquí; usa / o @, suelta/pega archivos; {0} nueva línea prompt.button.send=Enviar prompt.button.stop=Detener prompt.button.send.tooltip.stop=Para detener, presiona {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index ae5d807b587..2f437d5df60 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens utilisés session.header.context.reserved={0} réservés pour la sortie session.header.context.available={0} disponibles -prompt.placeholder=Saisir un message… -prompt.placeholder.with.shortcuts=Saisir un message… ({0} pour envoyer, {1} pour nouvelle ligne) -prompt.placeholder.with.send=Saisir un message… ({0} pour envoyer) -prompt.placeholder.with.newline=Saisir un message… ({0} pour nouvelle ligne) +prompt.placeholder=Tapez ici ; utilisez / ou @, déposez/collez des fichiers +prompt.placeholder.with.shortcuts=Tapez ici ; utilisez / ou @, déposez/collez des fichiers ; {0} envoyer, {1} nouvelle ligne +prompt.placeholder.with.send=Tapez ici ; utilisez / ou @, déposez/collez des fichiers ; {0} envoyer +prompt.placeholder.with.newline=Tapez ici ; utilisez / ou @, déposez/collez des fichiers ; {0} nouvelle ligne prompt.button.send=Envoyer prompt.button.stop=Arrêter prompt.button.send.tooltip.stop=Pour arrêter, appuyez sur {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 15903cd9bcf..552523ec0f3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1}トークン使用中 session.header.context.reserved={0}は出力用に予約済み session.header.context.available={0}利用可能 -prompt.placeholder=メッセージを入力… -prompt.placeholder.with.shortcuts=メッセージを入力…({0}で送信、{1}で改行) -prompt.placeholder.with.send=メッセージを入力…({0}で送信) -prompt.placeholder.with.newline=メッセージを入力…({0}で改行) +prompt.placeholder=ここに入力;/ または @ を使用、ファイルをドロップ/貼り付け +prompt.placeholder.with.shortcuts=ここに入力;/ または @ を使用、ファイルをドロップ/貼り付け;{0}で送信、{1}で改行 +prompt.placeholder.with.send=ここに入力;/ または @ を使用、ファイルをドロップ/貼り付け;{0}で送信 +prompt.placeholder.with.newline=ここに入力;/ または @ を使用、ファイルをドロップ/貼り付け;{0}で改行 prompt.button.send=送信 prompt.button.stop=停止 prompt.button.send.tooltip.stop=停止するには{0}を押してください diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 11e6f17224c..4c22c1e52b8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} 토큰 사용 중 session.header.context.reserved={0} 출력용 예약됨 session.header.context.available={0} 사용 가능 -prompt.placeholder=메시지를 입력하세요… -prompt.placeholder.with.shortcuts=메시지를 입력하세요… ({0} 전송, {1} 새 줄) -prompt.placeholder.with.send=메시지를 입력하세요… ({0} 전송) -prompt.placeholder.with.newline=메시지를 입력하세요… ({0} 새 줄) +prompt.placeholder=여기에 입력; / 또는 @ 사용, 파일 드롭/붙여넣기 +prompt.placeholder.with.shortcuts=여기에 입력; / 또는 @ 사용, 파일 드롭/붙여넣기; {0} 전송, {1} 새 줄 +prompt.placeholder.with.send=여기에 입력; / 또는 @ 사용, 파일 드롭/붙여넣기; {0} 전송 +prompt.placeholder.with.newline=여기에 입력; / 또는 @ 사용, 파일 드롭/붙여넣기; {0} 새 줄 prompt.button.send=전송 prompt.button.stop=정지 prompt.button.send.tooltip.stop=정지하려면 {0}을 누르세요 @@ -89,7 +89,7 @@ model.picker.free=무료 model.picker.reset=모델을 기본값으로 재설정 reasoning.picker.tooltip=추론 노력 선택 -history.tab.local=로컈 +history.tab.local=로컬 history.tab.cloud=클라우드 history.back=뒤로 history.search.placeholder=세션 검색 @@ -98,8 +98,8 @@ history.loading=로딩 중… history.untitled=제목 없음 history.delete.text=삭제 history.delete.confirm.title=세션을 삭제하시겠습니까? -history.delete.confirm.message=로컈 기록에서 "{0}"을 삭제하시겠습니까? -history.delete.confirm.message.multiple=로컈 기록에서 {0}개의 세션을 삭제하시겠습니까? +history.delete.confirm.message=로컬 기록에서 "{0}"을 삭제하시겠습니까? +history.delete.confirm.message.multiple=로컬 기록에서 {0}개의 세션을 삭제하시겠습니까? history.rename.title=세션 이름 바꾸기 history.rename.prompt=새 세션 이름: history.cloud.load.more=더 불러오기 @@ -115,7 +115,7 @@ history.time.hours={0}시간 전 history.time.days={0}일 전 history.time.months={0}개월 전 history.time.years={0}년 전 -history.error.local=로컈 기록을 로드하지 못했습니다 +history.error.local=로컬 기록을 로드하지 못했습니다 history.error.cloud=클라우드 기록을 로드하지 못했습니다 history.error.local.delete=세션을 삭제하지 못했습니다 history.error.local.rename=세션의 이름을 바꾸지 못했습니다 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index 73868343a00..e130d8e5d48 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens gebruikt session.header.context.reserved={0} gereserveerd voor uitvoer session.header.context.available={0} beschikbaar -prompt.placeholder=Typ een bericht… -prompt.placeholder.with.shortcuts=Typ een bericht… ({0} om te verzenden, {1} voor nieuwe regel) -prompt.placeholder.with.send=Typ een bericht… ({0} om te verzenden) -prompt.placeholder.with.newline=Typ een bericht… ({0} voor nieuwe regel) +prompt.placeholder=Typ hier; gebruik / of @, sleep/plak bestanden +prompt.placeholder.with.shortcuts=Typ hier; gebruik / of @, sleep/plak bestanden; {0} verzenden, {1} nieuwe regel +prompt.placeholder.with.send=Typ hier; gebruik / of @, sleep/plak bestanden; {0} verzenden +prompt.placeholder.with.newline=Typ hier; gebruik / of @, sleep/plak bestanden; {0} nieuwe regel prompt.button.send=Verzenden prompt.button.stop=Stoppen prompt.button.send.tooltip.stop=Druk op {0} om te stoppen diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index 96b88efb8be..e2185e84542 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens brukt session.header.context.reserved={0} reservert for utdata session.header.context.available={0} tilgjengelig -prompt.placeholder=Skriv en melding… -prompt.placeholder.with.shortcuts=Skriv en melding… ({0} for å sende, {1} for ny linje) -prompt.placeholder.with.send=Skriv en melding… ({0} for å sende) -prompt.placeholder.with.newline=Skriv en melding… ({0} for ny linje) +prompt.placeholder=Skriv her; bruk / eller @, slipp/lim inn filer +prompt.placeholder.with.shortcuts=Skriv her; bruk / eller @, slipp/lim inn filer; {0} send, {1} ny linje +prompt.placeholder.with.send=Skriv her; bruk / eller @, slipp/lim inn filer; {0} send +prompt.placeholder.with.newline=Skriv her; bruk / eller @, slipp/lim inn filer; {0} ny linje prompt.button.send=Send prompt.button.stop=Stopp prompt.button.send.tooltip.stop=Trykk {0} for å stoppe diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index ab1e87d414c..eff0dfc7e4c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokenów użito session.header.context.reserved={0} zarezerwowane dla wyjścia session.header.context.available={0} dostępne -prompt.placeholder=Wpisz wiadomość… -prompt.placeholder.with.shortcuts=Wpisz wiadomość… ({0} aby wyslać, {1} nowa linia) -prompt.placeholder.with.send=Wpisz wiadomość… ({0} aby wyslać) -prompt.placeholder.with.newline=Wpisz wiadomość… ({0} nowa linia) +prompt.placeholder=Pisz tutaj; użyj / lub @, upuść/wklej pliki +prompt.placeholder.with.shortcuts=Pisz tutaj; użyj / lub @, upuść/wklej pliki; {0} wyślij, {1} nowa linia +prompt.placeholder.with.send=Pisz tutaj; użyj / lub @, upuść/wklej pliki; {0} wyślij +prompt.placeholder.with.newline=Pisz tutaj; użyj / lub @, upuść/wklej pliki; {0} nowa linia prompt.button.send=Wyślij prompt.button.stop=Zatrzymaj prompt.button.send.tooltip.stop=Aby zatrzymać, naciśnij {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 12ccea438a4..efdff77ef96 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} tokens utilizados session.header.context.reserved={0} reservados para saída session.header.context.available={0} disponíveis -prompt.placeholder=Digite uma mensagem… -prompt.placeholder.with.shortcuts=Digite uma mensagem… ({0} para enviar, {1} para nova linha) -prompt.placeholder.with.send=Digite uma mensagem… ({0} para enviar) -prompt.placeholder.with.newline=Digite uma mensagem… ({0} para nova linha) +prompt.placeholder=Digite aqui; use / ou @, solte/cole arquivos +prompt.placeholder.with.shortcuts=Digite aqui; use / ou @, solte/cole arquivos; {0} enviar, {1} nova linha +prompt.placeholder.with.send=Digite aqui; use / ou @, solte/cole arquivos; {0} enviar +prompt.placeholder.with.newline=Digite aqui; use / ou @, solte/cole arquivos; {0} nova linha prompt.button.send=Enviar prompt.button.stop=Parar prompt.button.send.tooltip.stop=Para parar, pressione {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index 18e6b7426b3..0eeee04d8c9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} токенов использовано session.header.context.reserved={0} зарезервировано для вывода session.header.context.available={0} доступно -prompt.placeholder=Введите сообщение… -prompt.placeholder.with.shortcuts=Введите сообщение… ({0} — отправить, {1} — новая строка) -prompt.placeholder.with.send=Введите сообщение… ({0} — отправить) -prompt.placeholder.with.newline=Введите сообщение… ({0} — новая строка) +prompt.placeholder=Пишите здесь; используйте / или @, перетащите/вставьте файлы +prompt.placeholder.with.shortcuts=Пишите здесь; используйте / или @, перетащите/вставьте файлы; {0} — отправить, {1} — новая строка +prompt.placeholder.with.send=Пишите здесь; используйте / или @, перетащите/вставьте файлы; {0} — отправить +prompt.placeholder.with.newline=Пишите здесь; используйте / или @, перетащите/вставьте файлы; {0} — новая строка prompt.button.send=Отправить prompt.button.stop=Остановить prompt.button.send.tooltip.stop=Для остановки нажмите {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index eb47e0dc50c..a5184f76a91 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -69,10 +69,10 @@ session.header.context.used=ใช้แล้ว {0} / {1} โทเคน session.header.context.reserved={0} สำรองไว้สำหรับเอาต์พุต session.header.context.available={0} พร้อมใช้งาน -prompt.placeholder=พิมพ์ข้อความ… -prompt.placeholder.with.shortcuts=พิมพ์ข้อความ… ({0} เพื่อส่ง {1} สำหรับบรรทัดใหม่) -prompt.placeholder.with.send=พิมพ์ข้อความ… ({0} เพื่อส่ง) -prompt.placeholder.with.newline=พิมพ์ข้อความ… ({0} สำหรับบรรทัดใหม่) +prompt.placeholder=พิมพ์ที่นี่; ใช้ / หรือ @, ลากวาง/วางไฟล์ +prompt.placeholder.with.shortcuts=พิมพ์ที่นี่; ใช้ / หรือ @, ลากวาง/วางไฟล์; {0} ส่ง, {1} บรรทัดใหม่ +prompt.placeholder.with.send=พิมพ์ที่นี่; ใช้ / หรือ @, ลากวาง/วางไฟล์; {0} ส่ง +prompt.placeholder.with.newline=พิมพ์ที่นี่; ใช้ / หรือ @, ลากวาง/วางไฟล์; {0} บรรทัดใหม่ prompt.button.send=ส่ง prompt.button.stop=หยุด prompt.button.send.tooltip.stop=กด {0} เพื่อหยุด diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index d6ee2717f8d..f9efbf5b189 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -14,7 +14,7 @@ feedback.dialog.message=Geri bildiriminizi almaktan veya yaşadığınız sorunl feedback.dialog.github=GitHub'da sorun bildirin feedback.dialog.discord=Discord topluluğumuza katılın feedback.dialog.support=Müşteri Desteği -session.scroll.bottom=En alta kaýr +session.scroll.bottom=En alta kaydır session.copy.hover=Kopyala session.copy.copied=Kopyalandı session.tab.new=Yeni oturum @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} jeton kullanıldı session.header.context.reserved={0} çıkış için ayrıldı session.header.context.available={0} kullanılabilir -prompt.placeholder=Bir mesaj yazın… -prompt.placeholder.with.shortcuts=Bir mesaj yazın… ({0} göndermek için, {1} yeni satır için) -prompt.placeholder.with.send=Bir mesaj yazın… ({0} göndermek için) -prompt.placeholder.with.newline=Bir mesaj yazın… ({0} yeni satır için) +prompt.placeholder=Buraya yazın; / veya @ kullanın, dosyaları bırakın/yapıştırın +prompt.placeholder.with.shortcuts=Buraya yazın; / veya @ kullanın, dosyaları bırakın/yapıştırın; {0} gönder, {1} yeni satır +prompt.placeholder.with.send=Buraya yazın; / veya @ kullanın, dosyaları bırakın/yapıştırın; {0} gönder +prompt.placeholder.with.newline=Buraya yazın; / veya @ kullanın, dosyaları bırakın/yapıştırın; {0} yeni satır prompt.button.send=Gönder prompt.button.stop=Durdur prompt.button.send.tooltip.stop=Durdurmak için {0} basın diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index 624fc3ba051..6d3efd7480c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -52,7 +52,7 @@ session.error.compact=Помилка стиснення сесії session.error.unknown=Невідома помилка session.header.tokens=Токени -session.header.tokens.description=Токени, використані останній відповід䍎ю асистента: вхід, вихід, запис у кеш і читання з кешу. +session.header.tokens.description=Токени, використані останньою відповіддю асистента: вхід, вихід, запис у кеш і читання з кешу. session.header.input=вхід {0} session.header.output=вихід {0} session.header.cache.write=запис у кеш {0} @@ -69,10 +69,10 @@ session.header.context.used={0} / {1} токенів використано session.header.context.reserved={0} зарезервовано для виходу session.header.context.available={0} доступно -prompt.placeholder=Введіть повідомлення… -prompt.placeholder.with.shortcuts=Введіть повідомлення… ({0} — надіслати, {1} — новий рядок) -prompt.placeholder.with.send=Введіть повідомлення… ({0} — надіслати) -prompt.placeholder.with.newline=Введіть повідомлення… ({0} — новий рядок) +prompt.placeholder=Пишіть тут; використовуйте / або @, перетягніть/вставте файли +prompt.placeholder.with.shortcuts=Пишіть тут; використовуйте / або @, перетягніть/вставте файли; {0} — надіслати, {1} — новий рядок +prompt.placeholder.with.send=Пишіть тут; використовуйте / або @, перетягніть/вставте файли; {0} — надіслати +prompt.placeholder.with.newline=Пишіть тут; використовуйте / або @, перетягніть/вставте файли; {0} — новий рядок prompt.button.send=Надіслати prompt.button.stop=Зупинити prompt.button.send.tooltip.stop=Щоб зупинити, натисніть {0} diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index d1aa771eedc..eea4f6d35a0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -69,10 +69,10 @@ session.header.context.used=已使用 {0} / {1} 令牌 session.header.context.reserved={0} 为输出预留 session.header.context.available={0} 可用 -prompt.placeholder=输入消息… -prompt.placeholder.with.shortcuts=输入消息…({0} 发送,{1} 换行) -prompt.placeholder.with.send=输入消息…({0} 发送) -prompt.placeholder.with.newline=输入消息…({0} 换行) +prompt.placeholder=在此输入;使用 / 或 @,拖放/粘贴文件 +prompt.placeholder.with.shortcuts=在此输入;使用 / 或 @,拖放/粘贴文件;{0} 发送,{1} 换行 +prompt.placeholder.with.send=在此输入;使用 / 或 @,拖放/粘贴文件;{0} 发送 +prompt.placeholder.with.newline=在此输入;使用 / 或 @,拖放/粘贴文件;{0} 换行 prompt.button.send=发送 prompt.button.stop=停止 prompt.button.send.tooltip.stop=按 {0} 停止 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index b9a1ef9e9b1..4c771f8861f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -1,7 +1,7 @@ session.connection.connecting=載入中… session.connection.error.app=連線失敗 session.connection.error.workspace=工作區載入失敗 -session.connection.error.unknown=未知預設 +session.connection.error.unknown=未知錯誤 session.connection.retry=重試 session.connection.warning.config=設定警告 @@ -21,7 +21,7 @@ session.tab.new=新建工作階段 session.tab.untitled=未命名的工作階段 session.permission.title=權限請求 -session.permission.meta=工具:{0} • 樣式:{1} +session.permission.meta=工具:{0} • 模式:{1} session.permission.allow=允許 session.permission.deny=拒絕 session.question.dismiss=關閉 @@ -69,10 +69,10 @@ session.header.context.used=已使用 {0} / {1} 記號 session.header.context.reserved={0} 為輸出預留 session.header.context.available={0} 可用 -prompt.placeholder=輸入訊息… -prompt.placeholder.with.shortcuts=輸入訊息…({0} 發送,{1} 換行) -prompt.placeholder.with.send=輸入訊息…({0} 發送) -prompt.placeholder.with.newline=輸入訊息…({0} 換行) +prompt.placeholder=在此輸入;使用 / 或 @,拖放/貼上檔案 +prompt.placeholder.with.shortcuts=在此輸入;使用 / 或 @,拖放/貼上檔案;{0} 發送,{1} 換行 +prompt.placeholder.with.send=在此輸入;使用 / 或 @,拖放/貼上檔案;{0} 發送 +prompt.placeholder.with.newline=在此輸入;使用 / 或 @,拖放/貼上檔案;{0} 換行 prompt.button.send=發送 prompt.button.stop=停止 prompt.button.send.tooltip.stop=按 {0} 停止 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt index b996f0c8153..5300e74ae8b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.testing.FakeWorkspaceRpcApi import ai.kilocode.rpc.dto.WorkspaceFileDto import com.intellij.testFramework.fixtures.BasePlatformTestCase import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -67,4 +68,22 @@ class KiloWorkspaceServiceTest : BasePlatformTestCase() { assertFalse(ok) assertEquals(listOf("/test/.kilo/plans/a.md"), rpc.opened) } + + fun `test searchFiles rethrows cancellation`() = runBlocking { + val err = CancellationException("stale completion") + rpc.search = { throw err } + + val seen = try { + withContext(Dispatchers.Default) { + service.searchFiles("/test", "dep") + } + fail("expected cancellation") + null + } catch (e: CancellationException) { + e + } + + assertEquals(err.message, seen?.message) + assertEquals(listOf("dep"), rpc.searchQueries) + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt index 0ff015a00d2..73579e264bb 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionSidePanelManagerTest.kt @@ -152,7 +152,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { val second = active(manager) assertSame(first, second) - assertEquals(listOf("/test" to "ses_1", "/test" to null), created) + assertTrue(created.containsAll(listOf("/test" to "ses_1", "/test" to null))) } fun `test prompted blank session is reused from recents`() { @@ -382,6 +382,20 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { assertSame(first, active(manager)) } + fun `test history back restores latest session focus`() { + useLongInactiveDisposeTimeout() + val requests = mutableListOf() + val manager = manager(request = { requests.add(it) }) + + manager.openSession(session("ses_1")) + val first = active(manager) as SessionUi + manager.showHistory() + requests.clear() + back(manager) + + assertSame(first.defaultFocusedComponent, requests.single()) + } + fun `test history back without latest session opens new session`() { val manager = manager() @@ -416,7 +430,25 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { open(SessionRef.Local(session("ses_1"))) assertTrue(active(manager) is SessionUi) - assertEquals(listOf("/test" to "ses_1"), created) + assertTrue(created.contains("/test" to "ses_1")) + } + + fun `test opening local history item restores session focus`() { + lateinit var open: (SessionRef) -> Unit + val requests = mutableListOf() + val manager = manager( + history = { _, fn, _ -> + open = fn + JLabel("History") + }, + request = { requests.add(it) }, + ) + + manager.showHistory() + open(SessionRef.Local(session("ses_1"))) + val active = active(manager) as SessionUi + + assertSame(active.defaultFocusedComponent, requests.single()) } fun `test opening cloud history item shows session ui before import`() { @@ -625,6 +657,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { private fun manager( history: ((com.intellij.openapi.Disposable, (SessionRef) -> Unit, (String) -> Unit) -> JComponent)? = null, + request: (JComponent) -> Unit = {}, ): SessionSidePanelManager { val manager = SessionSidePanelManager( project = project, @@ -656,6 +689,7 @@ class SessionSidePanelManagerTest : BasePlatformTestCase() { status = { sessions.activity() }, history = history, timers = timers, + request = request, ) managers.add(manager) return manager diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt new file mode 100644 index 00000000000..21b97350835 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/CommandLifecycleTest.kt @@ -0,0 +1,69 @@ +package ai.kilocode.client.session.controller + +import ai.kilocode.client.session.model.SessionState +import ai.kilocode.rpc.dto.CommandDto +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto + +class CommandLifecycleTest : SessionControllerTestBase() { + + fun `test command creates new session and calls RPC`() { + appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy"))) + val m = controller() + + flush() + edt { m.command("deploy", "prod") } + flush() + + assertEquals(1, rpc.creates) + assertEquals(1, rpc.commands.size) + val call = rpc.commands.single() + assertEquals("ses_test", call.id) + assertEquals("/test", call.directory) + assertEquals("deploy", call.command) + assertEquals("prod", call.arguments) + } + + fun `test command reuses existing session`() { + val (m, _, _) = prompted() + val created = rpc.creates + + edt { m.command("deploy", "prod") } + flush() + + assertEquals(created, rpc.creates) + assertEquals("ses_test", rpc.commands.single().id) + } + + fun `test command records telemetry`() { + appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy"))) + val m = controller() + + flush() + edt { m.command("deploy", "prod") } + flush() + + val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" } + assertEquals("command", sent.properties["source"]) + val message = appRpc.telemetry.single { it.event == "Conversation Message" } + assertEquals("command", message.properties["source"]) + } + + fun `test command errors set state and telemetry`() { + appRpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto(model = "kilo/gpt-5")) + projectRpc.state.value = workspaceReady().copy(commands = listOf(CommandDto("deploy"))) + rpc.commandThrows = IllegalStateException("boom") + val m = controller() + + flush() + edt { m.command("deploy", "prod") } + flush() + + assertTrue(m.model.state is SessionState.Error) + val event = appRpc.telemetry.single { it.event == "Session Error" } + assertEquals("command", event.properties["context"]) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt index 698dec3f5e0..e7b24845e52 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionCreationTest.kt @@ -43,6 +43,34 @@ class SessionCreationTest : SessionControllerTestBase() { assertEquals("ses_test", rpc.prompts[1].first) } + fun `test same-turn first prompts share session creation`() { + val m = controller() + + edt { + m.prompt("first") + m.prompt("second") + } + flush() + + assertEquals(1, rpc.creates) + assertEquals(listOf("ses_test", "ses_test"), rpc.prompts.map { it.first }) + assertEquals(listOf("first", "second"), rpc.prompts.map { it.third.parts.single().text.toString() }.sorted()) + } + + fun `test same-turn first prompt and command share session creation`() { + val m = controller() + + edt { + m.prompt("first") + m.command("deploy", "prod") + } + flush() + + assertEquals(1, rpc.creates) + assertEquals("ses_test", rpc.prompts.single().first) + assertEquals("ses_test", rpc.commands.single().id) + } + fun `test prompt with existing ID skips creation`() { val m = controller("existing") collect(m) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt index 8d33870f7f3..cc2c1abe24e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/WorkspaceWatchingTest.kt @@ -1,6 +1,8 @@ package ai.kilocode.client.session.controller import ai.kilocode.rpc.dto.AgentDto +import ai.kilocode.rpc.dto.ModelDto +import ai.kilocode.rpc.dto.ProviderDto class WorkspaceWatchingTest : SessionControllerTestBase() { @@ -10,13 +12,28 @@ class WorkspaceWatchingTest : SessionControllerTestBase() { flush() events.clear() - projectRpc.state.value = workspaceReady() + projectRpc.state.value = workspaceReady( + providers = listOf( + ProviderDto( + id = "kilo", + name = "Kilo", + models = mapOf( + "gpt-5" to ModelDto( + id = "gpt-5", + name = "GPT-5", + mayTrainOnYourPrompts = true, + ), + ), + ), + ), + ) flush() assertEquals(1, m.model.agents.size) assertEquals("code", m.model.agents[0].name) assertEquals(1, m.model.models.size) assertEquals("gpt-5", m.model.models[0].id) + assertTrue(m.model.models[0].mayTrainOnYourPrompts) assertFalse(m.model.isReady()) assertControllerEvents(""" AccountOverlayChanged show loggedIn=false diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt index ab1ea65096b..075c26fc9a2 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt @@ -9,6 +9,8 @@ import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PartDto +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto import ai.kilocode.rpc.dto.PartTimeDto import ai.kilocode.rpc.dto.SessionDto import ai.kilocode.rpc.dto.SessionTimeDto @@ -154,6 +156,88 @@ class SessionModelTest : BasePlatformTestCase() { assertEquals("ContentRemoved m1/p1", events.single().toString()) } + fun `test updateContent skips user synthetic text`() { + model.addMessage(msg("m1", "user")) + events.clear() + + model.updateContent("m1", part("p1", "m1", "text", text = "raw content", synthetic = true)) + + assertNull(model.message("m1")!!.parts["p1"]) + assertTrue(events.isEmpty()) + } + + fun `test updateContent removes existing user text when marked synthetic`() { + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "visible")) + events.clear() + + model.updateContent("m1", part("p1", "m1", "text", text = "hidden", synthetic = true)) + + assertNull(model.message("m1")!!.parts["p1"]) + assertEquals("ContentRemoved m1/p1", events.single().toString()) + } + + fun `test assistant synthetic text remains visible`() { + model.addMessage(msg("m1", "assistant")) + events.clear() + + model.updateContent("m1", part("p1", "m1", "text", text = "visible", synthetic = true)) + + assertEquals("visible", (model.message("m1")!!.parts["p1"] as Text).content.toString()) + assertEquals("ContentAdded m1/p1", events.single().toString()) + } + + fun `test updateContent non synthetic text unhides previous synthetic user text`() { + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "hidden", synthetic = true)) + events.clear() + + model.updateContent("m1", part("p1", "m1", "text", text = "visible now")) + + assertModel(""" + user#m1 + text#p1: + visible now + """) + assertEquals("ContentAdded m1/p1", events.single().toString()) + } + + fun `test removeMessage clears hiddenText so a later non-synthetic part renders`() { + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "hidden", synthetic = true)) + + model.removeMessage("m1") + events.clear() + + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "visible now")) + + assertModel(""" + user#m1 + text#p1: + visible now + """) + assertEquals("ContentAdded m1/p1", events.last().toString()) + } + + fun `test clear resets hiddenText so a later non-synthetic part renders`() { + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "hidden", synthetic = true)) + + model.clear() + events.clear() + + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "visible now")) + + assertModel(""" + user#m1 + text#p1: + visible now + """) + assertEquals("ContentAdded m1/p1", events.last().toString()) + } + fun `test updateContent reasoning creates Reasoning content`() { model.addMessage(msg("m1", "assistant")) @@ -387,6 +471,17 @@ class SessionModelTest : BasePlatformTestCase() { assertTrue(events[1] is SessionModelEvent.ContentDelta) } + fun `test appendDelta ignores hidden synthetic user text part`() { + model.addMessage(msg("m1", "user")) + model.updateContent("m1", part("p1", "m1", "text", text = "hidden", synthetic = true)) + events.clear() + + model.appendDelta("m1", "p1", "still hidden") + + assertNull(model.message("m1")!!.parts["p1"]) + assertTrue(events.isEmpty()) + } + fun `test appendDelta on tool content is noop`() { model.addMessage(msg("m1", "assistant")) model.updateContent("m1", part("p1", "m1", "tool", tool = "bash", state = "running")) @@ -579,6 +674,32 @@ class SessionModelTest : BasePlatformTestCase() { assertTrue(entry.parts["p3"] is StepFinish) } + fun `test loadHistory skips user synthetic text`() { + model.loadHistory(listOf(MessageWithPartsDto( + msg("m1", "user"), + listOf( + part("p1", "m1", "text", text = "visible"), + part("p2", "m1", "text", text = "hidden", synthetic = true), + ), + ))) + + assertModel(""" + user#m1 + text#p1: + visible + """) + } + + fun `test file attachment preserves source metadata`() { + val source = PartSourceDto("file", PartSourceTextDto("@src/a.kt", 0.0, 9.0), path = "src/a.kt") + model.addMessage(msg("m1", "user")) + + model.updateContent("m1", filePart("f1", "m1", "text/plain", "file:///tmp/a.kt", "a.kt", source)) + + val file = model.message("m1")!!.parts["f1"] as FileAttachment + assertEquals(source, file.source) + } + fun `test updateContent drops patch parts`() { model.addMessage(msg("m1", "assistant")) events.clear() @@ -897,6 +1018,8 @@ class SessionModelTest : BasePlatformTestCase() { mime: String? = null, url: String? = null, filename: String? = null, + synthetic: Boolean? = null, + source: PartSourceDto? = null, ) = PartDto( id = id, sessionID = "ses", @@ -906,6 +1029,8 @@ class SessionModelTest : BasePlatformTestCase() { mime = mime, url = url, filename = filename, + synthetic = synthetic, + source = source, tool = tool, state = state, title = title, @@ -921,13 +1046,14 @@ class SessionModelTest : BasePlatformTestCase() { tokens = tokens, ) - private fun filePart(id: String, mid: String, mime: String, url: String, filename: String) = part( + private fun filePart(id: String, mid: String, mime: String, url: String, filename: String, source: PartSourceDto? = null) = part( id = id, mid = mid, type = "file", mime = mime, url = url, filename = filename, + source = source, ) private fun question(id: String) = Question( diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt index f266200cf94..bac03d4c9ca 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/PromptPanelTest.kt @@ -1,29 +1,56 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.model.PromptAttachment import ai.kilocode.client.session.ui.attachment.AttachmentCard import ai.kilocode.client.session.ui.attachment.AttachmentCardItem import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.session.ui.prompt.KiloPromptCompletionProvider +import ai.kilocode.client.session.ui.prompt.MentionAction import ai.kilocode.client.session.ui.prompt.PROMPT_ATTACHMENT_PASTE_HANDLER_KEY import ai.kilocode.client.session.ui.prompt.PromptAttachmentPasteHandler import ai.kilocode.client.session.ui.prompt.PromptAttachmentPasteProvider import ai.kilocode.client.session.ui.prompt.PromptDataKeys import ai.kilocode.client.session.ui.prompt.PromptPanel +import ai.kilocode.client.session.ui.prompt.SlashAction import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.test.CopyProviderSink +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.FileSearchResultDto +import ai.kilocode.rpc.dto.PromptPartDto +import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.ide.actions.UndoRedoAction import com.intellij.icons.AllIcons +import com.intellij.codeInsight.lookup.Lookup +import com.intellij.codeInsight.lookup.LookupManager +import com.intellij.codeInsight.lookup.LookupPositionStrategy +import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.notification.Notification import com.intellij.notification.Notifications +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.actionSystem.PlatformCoreDataKeys import com.intellij.openapi.actionSystem.UiDataProvider +import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.actions.PasteAction +import com.intellij.openapi.editor.colors.CodeInsightColors +import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.keymap.KeymapUtil import com.intellij.testFramework.PlatformTestUtil @@ -35,7 +62,11 @@ import com.intellij.util.Producer import com.intellij.util.ui.EmptyIcon import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import java.awt.Container import java.awt.BorderLayout import java.awt.datatransfer.DataFlavor @@ -56,11 +87,22 @@ import javax.swing.SwingUtilities @Suppress("UnstableApiUsage") class PromptPanelTest : BasePlatformTestCase() { private val roots = mutableListOf() + private lateinit var scope: CoroutineScope + private lateinit var rpc: FakeWorkspaceRpcApi + private lateinit var workspaces: KiloWorkspaceService + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + rpc = FakeWorkspaceRpcApi() + workspaces = KiloWorkspaceService(scope, rpc) + } override fun tearDown() { try { roots.asReversed().forEach { it.removeNotify() } roots.clear() + scope.cancel() } finally { super.tearDown() } @@ -200,6 +242,224 @@ class PromptPanelTest : BasePlatformTestCase() { assertFalse(editor.settings.isPaintSoftWraps) } + fun `test prompt editor highlights validated commands and mentions`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + rpc.fileResolver = { emptyList() } + + realize(panel, 260, 400) + field.text = "/new use ${MentionAction.GIT_CHANGES.token} and @unknown " + field.getEditor(false)!!.caretModel.moveToOffset(field.text.length) + panel.refreshHighlights() + waitForSend { spans(field).any { it.first == "@unknown" } } + + val spans = spans(field) + assertTrue(spans.contains("/new" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains(MentionAction.GIT_CHANGES.token to DefaultLanguageHighlighterColors.METADATA)) + assertTrue(spans.contains("@unknown" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES)) + } + + fun `test prompt editor exposes file editor for undo redo`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + val editor = field.getEditor(false)!! + WriteCommandAction.runWriteCommandAction(project) { + editor.document.insertString(0, "hello") + } + val sink = TestSink() + (field as UiDataProvider).uiDataSnapshot(sink) + val file = sink.file as? TextEditor ?: error("missing file editor") + + assertNotNull(file) + assertSame(editor.document, file.editor.document) + UndoManager.getInstance(project).undo(file) + assertEquals("", editor.document.text) + UndoManager.getInstance(project).redo(file) + assertEquals("hello", editor.document.text) + } + + fun `test prompt editor platform undo redo actions target prompt editor`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + val editor = field.getEditor(false)!! + WriteCommandAction.runWriteCommandAction(project) { + editor.document.insertString(0, "hello") + } + assertSame(true, editor.contentComponent.getClientProperty(UndoRedoAction.IGNORE_SWING_UNDO_MANAGER)) + val sink = TestSink() + (field as UiDataProvider).uiDataSnapshot(sink) + val file = sink.file as? TextEditor ?: error("missing file editor") + assertSame(editor.document, file.editor.document) + assertTrue("prompt file editor should have undo", UndoManager.getInstance(project).isUndoAvailable(file)) + + invokeAction(IdeActions.ACTION_UNDO, editor.contentComponent, file) + assertEquals("", editor.document.text) + invokeAction(IdeActions.ACTION_REDO, editor.contentComponent, file) + assertEquals("hello", editor.document.text) + } + + fun `test prompt editor component undo redo shortcuts target prompt editor`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + val editor = field.getEditor(false)!! + WriteCommandAction.runWriteCommandAction(project) { + editor.document.insertString(0, "hello") + } + + invokeComponentAction("Kilo Session Undo", editor) + assertEquals("", editor.document.text) + invokeComponentAction("Kilo Session Redo", editor) + assertEquals("hello", editor.document.text) + } + + fun `test prompt editor highlights missing mention as wrong reference`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + rpc.fileResolver = { emptyList() } + + realize(panel, 260, 400) + field.text = "@missing " + field.getEditor(false)!!.caretModel.moveToOffset(field.text.length) + panel.refreshHighlights() + waitForSend { spans(field).contains("@missing" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES) } + + assertTrue(spans(field).contains("@missing" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES)) + } + + fun `test accepted file mention highlights immediately`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/deploy.ts"))) + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "@dep" + val editor = field.getEditor(false)!! + editor.caretModel.moveToOffset(field.text.length) + + invokeCompletionAction(editor) + waitForLookupItems(editor) + acceptLookup(editor) + waitForSend { spans(field).contains("@src/deploy.ts" to DefaultLanguageHighlighterColors.METADATA) } + + assertTrue(spans(field).contains("@src/deploy.ts" to DefaultLanguageHighlighterColors.METADATA)) + } + + fun `test invalid edited file mention highlights after caret leaves token`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/deploy.ts"))) + rpc.fileResolver = { path -> if (path == "src/deploy.ts") listOf(file(path)) else emptyList() } + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "@dep" + val editor = field.getEditor(false)!! + editor.caretModel.moveToOffset(field.text.length) + invokeCompletionAction(editor) + waitForLookupItems(editor) + acceptLookup(editor) + waitForSend { spans(field).contains("@src/deploy.ts" to DefaultLanguageHighlighterColors.METADATA) } + + val offset = field.text.indexOf(' ') + WriteCommandAction.runWriteCommandAction(project) { + editor.document.insertString(offset, "x") + } + editor.caretModel.moveToOffset(offset + 1) + UIUtil.dispatchAllInvocationEvents() + editor.caretModel.moveToOffset(field.text.length) + waitForSend { spans(field).contains("@src/deploy.tsx" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES) } + + assertTrue(spans(field).contains("@src/deploy.tsx" to CodeInsightColors.WRONG_REFERENCES_ATTRIBUTES)) + } + + fun `test prompt clear removes prompt highlighters`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "use ${MentionAction.GIT_CHANGES.token}" + UIUtil.dispatchAllInvocationEvents() + assertEquals(1, field.getEditor(false)!!.markupModel.allHighlighters.size) + + panel.clear() + UIUtil.dispatchAllInvocationEvents() + + assertEquals(0, field.getEditor(false)!!.markupModel.allHighlighters.size) + } + + fun `test prompt highlighters stay bounded across edits`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + repeat(50) { + field.text = if (it % 2 == 0) { + "/new ${MentionAction.GIT_CHANGES.token}" + } else { + "/new ${MentionAction.GIT_CHANGES.token} now" + } + UIUtil.dispatchAllInvocationEvents() + assertTrue(field.getEditor(false)!!.markupModel.allHighlighters.size <= 2) + } + } + + fun `test prompt local completion shortcut opens mention lookup`() { + rpc.searchResult = ai.kilocode.rpc.dto.FileSearchResultDto( + files = listOf(ai.kilocode.rpc.dto.WorkspaceFileDto("src/deploy.ts", "deploy.ts")), + ) + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "@dep" + val editor = field.getEditor(false)!! + editor.caretModel.moveToOffset(field.text.length) + + invokeCompletionAction(editor) + val items = waitForLookupItems(editor) + + assertTrue("items=$items", items.contains("src/deploy.ts")) + } + + fun `test prompt local completion shortcut opens slash lookup`() { + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "/ne" + val editor = field.getEditor(false)!! + editor.caretModel.moveToOffset(field.text.length) + + invokeCompletionAction(editor) + val items = waitForLookupItems(editor) + + assertTrue("items=$items", items.contains("new")) + } + + fun `test prompt completion lookup is positioned above caret`() { + rpc.searchResult = FileSearchResultDto( + files = listOf(WorkspaceFileDto("src/deploy.ts", "deploy.ts")), + ) + val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }, completion = completion()) + val field = panel.defaultFocusedComponent as EditorTextField + + realize(panel, 260, 400) + field.text = "@dep" + val editor = field.getEditor(false)!! + editor.caretModel.moveToOffset(field.text.length) + + invokeCompletionAction(editor) + waitForLookupItems(editor) + val lookup = LookupManager.getActiveLookup(editor) as? LookupImpl ?: error("missing lookup") + + assertEquals(LookupPositionStrategy.ONLY_ABOVE, lookup.presentation.positionStrategy) + } + fun `test prompt editor shrinks when lines are removed`() { val panel = PromptPanel(project = project, onSend = { _, _ -> }, onAbort = {}, onEnhance = { _, _ -> }) val editor = panel.defaultFocusedComponent as EditorTextField @@ -285,6 +545,31 @@ class PromptPanelTest : BasePlatformTestCase() { assertTrue(sent) } + fun `test submit resolves mentions from current text`() { + var text: String? = null + var sent: List? = null + val part = PromptPartDto(type = "file", mime = "text/plain", url = "file:///repo/src/x.kt") + val panel = PromptPanel( + project = project, + onSend = { _, files -> sent = files }, + onAbort = {}, + onEnhance = { _, _ -> }, + onMentions = { value -> + text = value + listOf(part) + }, + ) + val editor = panel.defaultFocusedComponent as EditorTextField + panel.setReady(true) + + editor.text = "read @src/x.kt" + panel.send() + waitForSend { sent != null } + + assertEquals("read @src/x.kt", text) + assertEquals(listOf(part), sent) + } + fun `test clear removes attachments`() { val panel = PromptPanel(project, { _, _ -> }, {}, { _, _ -> }) @@ -778,6 +1063,97 @@ class PromptPanelTest : BasePlatformTestCase() { return root } + private fun completion() = KiloPromptCompletionProvider( + workspace = workspaces.workspace("/test"), + service = workspaces, + actions = listOf( + SlashAction(SlashAction.NEW.name, "New") {}, + SlashAction("next", "Next") {}, + ), + mentions = listOf(MentionAction( + MentionAction.GIT_CHANGES.name, + "Git Changes", + available = MentionAction.GIT_CHANGES.available, + )), + scope = scope, + ) + + private fun invokeCompletionAction(editor: Editor) { + val action = ActionUtil.getActions(editor.contentComponent).first { item -> + item.templatePresentation.text == "Kilo Prompt Completion" + } + val event = event(action, editor) + ActionUtil.updateAction(action, event) + ActionUtil.performAction(action, event) + } + + private fun invokeComponentAction(text: String, editor: Editor) { + val action = ActionUtil.getActions(editor.contentComponent).first { item -> + item.templatePresentation.text == text + } + val event = event(action, editor) + ActionUtil.updateAction(action, event) + assertTrue("action $text should be enabled", event.presentation.isEnabled) + ActionUtil.performAction(action, event) + UIUtil.dispatchAllInvocationEvents() + } + + private fun invokeAction(id: String, component: java.awt.Component, file: TextEditor) { + val action = ActionManager.getInstance().getAction(id) ?: error("missing action $id") + val ctx = DataContext { data -> + when (data) { + CommonDataKeys.PROJECT.name -> project + PlatformCoreDataKeys.CONTEXT_COMPONENT.name -> component + PlatformCoreDataKeys.FILE_EDITOR.name -> file + else -> null + } + } + val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) + ActionUtil.updateAction(action, event) + assertTrue("action $id should be enabled", event.presentation.isEnabled) + ActionUtil.performAction(action, event) + UIUtil.dispatchAllInvocationEvents() + } + + private fun waitForLookupItems(editor: Editor): List { + repeat(50) { + UIUtil.dispatchAllInvocationEvents() + val items = LookupManager.getActiveLookup(editor)?.items.orEmpty().map { item -> item.lookupString } + if (items.isNotEmpty()) return items + Thread.sleep(20) + } + return LookupManager.getActiveLookup(editor)?.items.orEmpty().map { it.lookupString } + } + + private fun acceptLookup(editor: Editor) { + val lookup = LookupManager.getActiveLookup(editor) as? LookupImpl ?: error("missing lookup") + lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR) + UIUtil.dispatchAllInvocationEvents() + } + + private fun file(path: String) = WorkspaceFileDto( + path = path, + name = path.substringAfterLast('/'), + ) + + private fun event(action: AnAction, editor: Editor): AnActionEvent { + val ctx = DataContext { id -> + when (id) { + CommonDataKeys.EDITOR.name -> editor + CommonDataKeys.PROJECT.name -> project + else -> null + } + } + return AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) + } + + private fun spans(field: EditorTextField): List> { + val editor = field.getEditor(false)!! + return editor.markupModel.allHighlighters.map { + field.text.substring(it.startOffset, it.endOffset) to it.textAttributesKey + } + } + private fun createEditor(): Editor { val factory = EditorFactory.getInstance() return factory.createEditor(factory.createDocument(""), project) @@ -843,10 +1219,12 @@ class PromptPanelTest : BasePlatformTestCase() { private class TestSink : CopyProviderSink() { var send: Any? = null + var file: Any? = null override fun set(key: DataKey, data: T?) { super.set(key, data) if (key == PromptDataKeys.SEND) send = data + if (key == PlatformCoreDataKeys.FILE_EDITOR) file = data } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt index af6a172429f..59ef3d9759f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionUiUpdateTest.kt @@ -15,6 +15,8 @@ import ai.kilocode.rpc.dto.MessageDto import ai.kilocode.rpc.dto.MessageTimeDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PartDto +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -264,6 +266,100 @@ class SessionUiUpdateTest : BasePlatformTestCase() { assertEquals(listOf("data:image/png;base64,aGVsbG8=", "data:text/plain;base64,aGVsbG8="), opened) } + fun `test user file mention hides synthetic read payload and text attachment card`() { + model.upsertMessage(msg("u1", "user")) + model.updateContent("u1", part("p1", "u1", "text", text = "read @src/a.kt")) + model.updateContent("u1", PartDto( + id = "p2", + sessionID = "ses", + messageID = "u1", + type = "text", + text = "/tmp/a.kt\nraw", + synthetic = true, + )) + model.updateContent("u1", PartDto( + id = "f1", + sessionID = "ses", + messageID = "u1", + type = "file", + mime = "text/plain", + url = "file:///tmp/a.kt", + filename = "a.kt", + source = source("src/a.kt"), + )) + + val msg = panel.findMessage("u1")!! + + assertEquals(listOf("p1"), msg.partIds()) + assertNull(msg.part("p2")) + assertNull(msg.part("f1")) + assertEquals("read [@src/a.kt](src/a.kt)", (msg.part("p1") as TextView).markdown()) + assertEquals(0, msg.components.filterIsInstance().size) + } + + fun `test user git changes mention hides synthetic data attachment card`() { + model.upsertMessage(msg("u1", "user")) + model.updateContent("u1", part("p1", "u1", "text", text = "review @git-changes")) + model.updateContent("u1", PartDto( + id = "f1", + sessionID = "ses", + messageID = "u1", + type = "file", + mime = "text/plain", + url = "data:text/plain;charset=utf-8,diff%20content", + filename = "git-changes.txt", + source = PartSourceDto( + type = "file", + text = PartSourceTextDto("@git-changes", 7.0, 19.0), + path = "git-changes", + ), + )) + + val msg = panel.findMessage("u1")!! + + assertEquals(listOf("p1"), msg.partIds()) + assertNull(msg.part("f1")) + assertEquals("review [@git-changes](git-changes)", (msg.part("p1") as TextView).markdown()) + assertEquals(0, msg.components.filterIsInstance().size) + } + + fun `test source less text attachment still renders in prompt strip`() { + model.upsertMessage(msg("u1", "user")) + model.updateContent("u1", PartDto( + id = "f1", + sessionID = "ses", + messageID = "u1", + type = "file", + mime = "text/plain", + url = "data:text/plain;base64,aGVsbG8=", + filename = "note.txt", + )) + + val view = panel.findMessage("u1")!!.part("f1") + + assertTrue(view is PromptAttachmentView) + assertNotNull(find(view!!, AttachmentCard::class.java)) + } + + fun `test source backed image attachment still renders in prompt strip`() { + model.upsertMessage(msg("u1", "user")) + model.updateContent("u1", PartDto( + id = "f1", + sessionID = "ses", + messageID = "u1", + type = "file", + mime = "image/png", + url = "file:///tmp/a.png", + filename = "a.png", + source = source("src/a.png"), + )) + + val view = panel.findMessage("u1")!!.part("f1") + + assertTrue(view is PromptAttachmentView) + assertNotNull(find(view!!, AttachmentCard::class.java)) + } + fun `test empty sanitized user text does not create prompt panel`() { model.upsertMessage(msg("u1", "user")) model.updateContent("u1", part("p1", "u1", "text", text = "read these screenshots")) @@ -461,6 +557,12 @@ class SessionUiUpdateTest : BasePlatformTestCase() { id = id, sessionID = "ses", messageID = mid, type = "tool", tool = tool, state = state, ) + private fun source(path: String) = PartSourceDto( + type = "file", + path = path, + text = PartSourceTextDto("@$path", 5.0, (6 + path.length).toDouble()), + ) + private fun find(root: java.awt.Component, type: Class): T? { if (type.isInstance(root)) return type.cast(root) if (root is Container) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index 056dcc7ba02..d34f6a541e5 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -265,20 +265,20 @@ class ModelPickerTest : BasePlatformTestCase() { assertEquals(listOf("low", "high"), item.variants) } - fun `test selected free model indicates data collection`() { + fun `test selected paid model with training flag indicates data collection`() { val picker = ModelPicker() - picker.setItems(listOf(item("auto", "Auto Free", "kilo", "Kilo", free = true))) + picker.setItems(listOf(item("paid", "Paid", "kilo", "Kilo", training = true))) assertFalse(picker.text.contains("Data may be used for training")) assertSame(ModelPickerRenderer.DATA_COLLECTED, picker.icon) assertEquals("Select model
The current selected model may be used for training", picker.toolTipText) } - fun `test selected non-kilo free model does not indicate data collection`() { + fun `test selected free model without training flag does not indicate data collection`() { val picker = ModelPicker() - picker.setItems(listOf(item("free", "OpenRouter Free", "openrouter", "OpenRouter", free = true))) + picker.setItems(listOf(item("free", "Free", "kilo", "Kilo", free = true))) assertNull(picker.icon) assertEquals("Select model", picker.toolTipText) @@ -393,11 +393,39 @@ class ModelPickerTest : BasePlatformTestCase() { assertTrue(renderer.badgeVisible()) assertEquals("Free", renderer.badgeText()) + assertFalse(renderer.warningVisible()) + } + + fun `test renderer shows data collection warning for paid training model`() { + val row = ModelPickerRow(item("paid", "Paid", "kilo", "Kilo", training = true), "Kilo", false) + val model = CollectionListModel(listOf(row)) + val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) + val list = JBList(model) + + renderer.getListCellRendererComponent(list, row, 0, false, false) + + assertFalse(renderer.badgeVisible()) assertTrue(renderer.warningVisible()) assertEquals("Data may be used for training", renderer.warningTooltip()) } - fun `test renderer hides data collection warning for non-kilo free model`() { + fun `test renderer shows BYOK instead of free when both are available`() { + val row = ModelPickerRow( + ModelPicker.Item("claude", "Claude", "kilo", "Kilo", free = true, byok = true), + "Kilo", + false, + ) + val model = CollectionListModel(listOf(row)) + val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) + val list = JBList(model) + + renderer.getListCellRendererComponent(list, row, 0, false, false) + + assertTrue(renderer.byokVisible()) + assertFalse(renderer.badgeVisible()) + } + + fun `test renderer hides data collection warning without training flag`() { val row = ModelPickerRow(ModelPicker.Item("free", "Free", "openrouter", "OpenRouter", free = true), "OpenRouter", false) val model = CollectionListModel(listOf(row)) val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) @@ -420,6 +448,7 @@ class ModelPickerTest : BasePlatformTestCase() { assertSame(EmptyIcon.ICON_16, renderer.starIcon()) assertFalse(renderer.badgeVisible()) + assertFalse(renderer.byokVisible()) } private fun item( @@ -429,7 +458,8 @@ class ModelPickerTest : BasePlatformTestCase() { name: String, index: Double? = null, free: Boolean = false, - ) = ModelPicker.Item(id, display, provider, name, index, free = free) + training: Boolean = false, + ) = ModelPicker.Item(id, display, provider, name, index, free = free, mayTrainOnYourPrompts = training) private fun favoriteInset(list: JBList<*>): Int { if (!NewUI.isEnabled()) return 0 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt new file mode 100644 index 00000000000..76077daeabb --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt @@ -0,0 +1,390 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.CommandDto +import ai.kilocode.rpc.dto.FileSearchResultDto +import ai.kilocode.rpc.dto.KiloWorkspaceStateDto +import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto +import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.codeInsight.lookup.LookupElementPresentation +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.textCompletion.TextCompletionUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +@Suppress("UnstableApiUsage") +class KiloPromptCompletionProviderTest : BasePlatformTestCase() { + private lateinit var scope: CoroutineScope + private lateinit var rpc: FakeWorkspaceRpcApi + private lateinit var provider: KiloPromptCompletionProvider + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + rpc = FakeWorkspaceRpcApi() + val workspaces = KiloWorkspaceService(scope, rpc) + provider = KiloPromptCompletionProvider( + workspace = workspaces.workspace("/test"), + service = workspaces, + actions = listOf( + SlashAction(SlashAction.NEW.name, "New", listOf("clear")) {}, + SlashAction("sessions", "Sessions", listOf("continue")) {}, + SlashAction("next", "Next") {}, + ), + mentions = listOf(MentionAction( + MentionAction.GIT_CHANGES.name, + "Git Changes", + available = MentionAction.GIT_CHANGES.available, + )), + scope = scope, + ) + } + + override fun tearDown() { + try { + scope.cancel() + } finally { + super.tearDown() + } + } + + fun `test mention completion shows backend fuzzy results without local filtering`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/foo/Bar.kt"))) + + complete("@sfb") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "src/foo/Bar.kt") + assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) + assertEquals(listOf("sfb"), rpc.searchQueries) + } + + fun `test mention completion opens in middle of token`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/deploy.ts"))) + + complete("@deploy") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "src/deploy.ts") + assertEquals(listOf("dep"), rpc.searchQueries) + } + + fun `test slash completion opens in middle of token`() { + complete("/new") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "new") + assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) + } + + fun `test mention completion reuses identical prefix result`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/Main.kt"))) + + complete("@main") + complete("@main") + + assertEquals(listOf("main"), rpc.searchQueries) + } + + fun `test clearing mentions resets cached prefix result`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("src/Main.kt"))) + + complete("@main") + provider.clearMentions() + complete("@main") + + assertEquals(listOf("main", "main"), rpc.searchQueries) + } + + fun `test mention completion includes matching special items`() { + rpc.searchResult = FileSearchResultDto(git = true) + + complete("@git") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), MentionAction.GIT_CHANGES.name) + assertEquals(listOf("git"), rpc.searchQueries) + } + + fun `test mention completion keeps no-match placeholder`() { + rpc.searchResult = FileSearchResultDto() + + complete("@zzz") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), noMatches()) + assertFalse(myFixture.lookupElementStrings.orEmpty().contains("src/foo/Bar.kt")) + assertEquals(listOf("zzz"), rpc.searchQueries) + } + + fun `test accepting mention no-match placeholder preserves prefix`() { + rpc.searchResult = FileSearchResultDto() + + complete("@zzz") + myFixture.type('\n') + + assertEquals("@zzz", myFixture.editor.document.text) + assertNull(provider.mentionAt("@zzz", 1)) + } + + fun `test accepting mention mid token replaces glued suffix`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("backend/deploy-dev.sh"))) + + complete("@backend/deploy-dev.sh") + myFixture.type('\n') + + assertEquals("@backend/deploy-dev.sh ", myFixture.editor.document.text) + assertEquals( + KiloPromptCompletionProvider.MentionHit(0, 22, "backend/deploy-dev.sh", true), + provider.mentionAt(myFixture.editor.document.text, 1), + ) + } + + fun `test accepting mention mid token trims before trailing content`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("backend/deploy-dev.sh"))) + + complete("@backend/deploy-dev.sh tail") + myFixture.type('\n') + + assertEquals("@backend/deploy-dev.sh tail", myFixture.editor.document.text) + assertEquals( + KiloPromptCompletionProvider.MentionHit(0, 22, "backend/deploy-dev.sh", true), + provider.mentionAt(myFixture.editor.document.text, 1), + ) + } + + fun `test slash completion keeps no-match placeholder`() { + complete("/zzz") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), noMatches()) + } + + fun `test slash completion hides placeholder for real matches`() { + complete("/ne") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "new") + assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) + } + + fun `test slash completion matches client aliases`() { + complete("/cle") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "new") + assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) + } + + fun `test blank mention completion includes special and root entries`() { + rpc.searchResult = FileSearchResultDto( + files = listOf(file("src", directory = true), file("README.md")), + git = true, + ) + + complete("@") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), MentionAction.GIT_CHANGES.name, "src", "README.md") + val items = myFixture.lookupElementStrings.orEmpty() + assertEquals(MentionAction.GIT_CHANGES.name, items.first()) + assertEquals(listOf(""), rpc.searchQueries) + } + + fun `test prewarm serves blank mention completion from cache`() { + rpc.searchResult = FileSearchResultDto( + files = listOf(file("src", directory = true), file("README.md")), + git = true, + ) + + provider.prewarm() + waitFor { rpc.searchQueries.contains("") } + complete("@") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), MentionAction.GIT_CHANGES.name, "src", "README.md") + assertEquals(listOf(""), rpc.searchQueries) + } + + fun `test mention completion renders file type icons`() { + rpc.searchResult = FileSearchResultDto(files = listOf(file("image.png"), file("src", directory = true))) + + complete("@") + + assertEquals(FileTypeManager.getInstance().getFileTypeByFileName("image.png").icon ?: AllIcons.FileTypes.Text, icon("image.png")) + assertSame(AllIcons.Nodes.Folder, icon("src")) + } + + fun `test highlights known slash command at start`() { + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(0, 4, KiloPromptCompletionProvider.HighlightKind.COMMAND)), + provider.highlights("/new start fresh"), + ) + } + + fun `test highlights client slash command aliases at start`() { + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(0, 6, KiloPromptCompletionProvider.HighlightKind.COMMAND)), + provider.highlights("/clear"), + ) + } + + fun `test highlights server slash command at start`() { + rpc.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY, commands = listOf(CommandDto("deploy"))) + + waitFor { provider.highlights("/deploy prod").isNotEmpty() } + + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(0, 7, KiloPromptCompletionProvider.HighlightKind.COMMAND)), + provider.highlights("/deploy prod"), + ) + } + + fun `test highlights ignore unknown and non-leading slash commands`() { + assertTrue(provider.highlights("/bogus now").isEmpty()) + assertTrue(provider.highlights("hi /new").isEmpty()) + } + + fun `test highlights special mentions without tracked paths`() { + assertEquals( + listOf( + KiloPromptCompletionProvider.Highlight(4, 16, KiloPromptCompletionProvider.HighlightKind.MENTION), + ), + provider.highlights("use ${MentionAction.GIT_CHANGES.token}").sortedBy { it.start }, + ) + } + + fun `test serverCommand routes only known server commands`() { + rpc.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY, commands = listOf(CommandDto("deploy"))) + + waitFor { provider.serverCommand("/deploy x") != null } + + assertEquals("deploy" to "x", provider.serverCommand("/deploy x")) + assertNull(provider.serverCommand("/new")) + assertNull(provider.serverCommand("hi /deploy")) + assertNull(provider.serverCommand("/unknown")) + } + + fun `test clientAction resolves canonical names and aliases`() { + assertEquals(SlashAction.NEW.name, provider.clientAction("/new")?.name) + assertEquals(SlashAction.NEW.name, provider.clientAction("/clear")?.name) + assertEquals("sessions", provider.clientAction("/continue")?.name) + assertNull(provider.clientAction("hi /clear")) + } + + fun `test serverCommand does not route client aliases`() { + rpc.state.value = KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY, commands = listOf(CommandDto("clear"))) + + assertNull(provider.serverCommand("/clear")) + } + + fun `test highlights tracked mentions longest first`() { + addMention("src/a.ts", "@ts") + addMention("src/a.tsx", "@tsx") + + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(4, 14, KiloPromptCompletionProvider.HighlightKind.MENTION)), + provider.highlights("see @src/a.tsx"), + ) + } + + fun `test highlights unknown mentions are pending before validation`() { + assertTrue(provider.highlights("see @unknownPath").isEmpty()) + } + + fun `test highlights unresolved mention after validation`() { + var done = false + rpc.fileResolver = { emptyList() } + + provider.validate("see @unknownPath", -1) { done = true } + waitFor { done } + + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(4, 16, KiloPromptCompletionProvider.HighlightKind.INVALID)), + provider.highlights("see @unknownPath", -1), + ) + } + + fun `test highlights existing hand typed mention after validation`() { + var done = false + rpc.fileResolver = { path -> if (path == "src/x.kt") listOf(file(path)) else emptyList() } + + provider.validate("see @src/x.kt", -1) { done = true } + waitFor { done } + + assertEquals( + listOf(KiloPromptCompletionProvider.Highlight(4, 13, KiloPromptCompletionProvider.HighlightKind.MENTION)), + provider.highlights("see @src/x.kt", -1), + ) + } + + fun `test mention under caret is not flagged`() { + assertTrue(provider.highlights("@nope", caret = 5).isEmpty()) + } + + fun `test mentionAt resolves tracked file mention`() { + addMention("src/a.ts", "@ts") + + assertEquals( + KiloPromptCompletionProvider.MentionHit(4, 13, "src/a.ts", true), + provider.mentionAt("see @src/a.ts", 6), + ) + } + + fun `test mentionAt marks unresolved file mention after validation`() { + var done = false + rpc.fileResolver = { emptyList() } + + provider.validate("see @missing.ts", -1) { done = true } + waitFor { done } + + assertEquals( + KiloPromptCompletionProvider.MentionHit(4, 15, "missing.ts", false), + provider.mentionAt("see @missing.ts", 6), + ) + } + + fun `test mentionAt ignores special pending and out of range`() { + assertNull(provider.mentionAt("use ${MentionAction.GIT_CHANGES.token}", 6)) + assertNull(provider.mentionAt("see @unvalidated.ts", 6)) + assertNull(provider.mentionAt("hello world", 3)) + } + + fun `test navigate opens resolved mention file`() { + rpc.fileResolver = { path -> if (path == "src/a.ts") listOf(file(path)) else emptyList() } + + provider.navigate("src/a.ts") + waitFor { rpc.opened.contains("src/a.ts") } + + assertTrue(rpc.opened.contains("src/a.ts")) + } + + private fun complete(text: String) { + val file = myFixture.configureByText("prompt.txt", text) + TextCompletionUtil.installProvider(file, provider, true) + myFixture.completeBasic() + } + + private fun addMention(path: String, query: String) { + rpc.searchResult = FileSearchResultDto(files = listOf(file(path))) + complete("$query") + myFixture.type('\n') + assertEquals(path, provider.mentionAt("@$path", 1)?.value) + } + + private fun waitFor(done: () -> Boolean) { + repeat(50) { + com.intellij.util.ui.UIUtil.dispatchAllInvocationEvents() + if (done()) return + Thread.sleep(20) + } + } + + private fun icon(value: String) = LookupElementPresentation().also { item(value).renderElement(it) }.icon + + private fun item(value: String) = myFixture.lookupElements.orEmpty().first { it.lookupString == value } + + private fun file(path: String, directory: Boolean = false) = WorkspaceFileDto( + path = path, + name = path.substringAfterLast('/'), + directory = directory, + ) + + private fun noMatches() = KiloBundle.message("prompt.completion.noMatches") +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt new file mode 100644 index 00000000000..78a62bac117 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/MentionNavigatorTest.kt @@ -0,0 +1,121 @@ +package ai.kilocode.client.session.ui.prompt + +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.WorkspaceFileDto +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.EditorMouseEvent +import com.intellij.openapi.editor.event.EditorMouseEventArea +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import java.awt.event.InputEvent +import java.awt.event.MouseEvent +import kotlin.test.fail + +@Suppress("UnstableApiUsage") +class MentionNavigatorTest : BasePlatformTestCase() { + // "see @src/a.ts and @missing.ts" — resolved token spans 4..13, unresolved spans 18..29. + private val text = "see @src/a.ts and @missing.ts" + + private lateinit var scope: CoroutineScope + private lateinit var rpc: FakeWorkspaceRpcApi + private lateinit var provider: KiloPromptCompletionProvider + private lateinit var editor: EditorEx + private lateinit var navigator: MentionNavigator + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + rpc = FakeWorkspaceRpcApi() + rpc.fileResolver = { path -> if (path == "src/a.ts") listOf(file(path)) else emptyList() } + val workspaces = KiloWorkspaceService(scope, rpc) + provider = KiloPromptCompletionProvider( + workspace = workspaces.workspace("/test"), + service = workspaces, + actions = emptyList(), + mentions = emptyList(), + scope = scope, + ) + val factory = EditorFactory.getInstance() + editor = factory.createEditor(factory.createDocument(text), project) as EditorEx + navigator = MentionNavigator(editor, provider) + navigator.install() + provider.validate(text, -1) {} + waitFor { + provider.mentionAt(text, 6)?.resolved == true && provider.mentionAt(text, 20)?.resolved == false + } + } + + override fun tearDown() { + try { + EditorFactory.getInstance().releaseEditor(editor) + scope.cancel() + } finally { + super.tearDown() + } + } + + fun `test hovering unresolved mention sets tooltip`() { + navigator.mouseMoved(event(20)) + + assertEquals(KiloBundle.message("prompt.mention.unresolved", "missing.ts"), editor.contentComponent.toolTipText) + } + + fun `test leaving mention clears tooltip`() { + navigator.mouseMoved(event(20)) + navigator.mouseMoved(event(1)) + + assertNull(editor.contentComponent.toolTipText) + } + + fun `test modifier hover highlights resolved mention as link`() { + navigator.mouseMoved(event(6, modifier = true)) + assertTrue(editor.markupModel.allHighlighters.any { it.layer == HighlighterLayer.HYPERLINK }) + + navigator.mouseMoved(event(6, modifier = false)) + assertFalse(editor.markupModel.allHighlighters.any { it.layer == HighlighterLayer.HYPERLINK }) + } + + fun `test modifier click opens resolved mention`() { + navigator.mouseClicked(event(6, modifier = true)) + waitFor { rpc.opened.contains("src/a.ts") } + + assertTrue(rpc.opened.contains("src/a.ts")) + } + + private fun event(offset: Int, modifier: Boolean = false): EditorMouseEvent { + val mask = if (modifier) InputEvent.META_DOWN_MASK or InputEvent.CTRL_DOWN_MASK else 0 + val point = editor.offsetToXY(offset) + val mouse = MouseEvent(editor.contentComponent, MouseEvent.MOUSE_MOVED, 0L, mask, point.x, point.y, 1, false) + return EditorMouseEvent( + editor, + mouse, + EditorMouseEventArea.EDITING_AREA, + offset, + editor.offsetToLogicalPosition(offset), + editor.offsetToVisualPosition(offset), + true, + null, + null, + null, + ) + } + + private fun file(path: String) = WorkspaceFileDto(path = path, name = path.substringAfterLast('/')) + + private fun waitFor(done: () -> Boolean) { + repeat(250) { + UIUtil.dispatchAllInvocationEvents() + if (done()) return + Thread.sleep(20) + } + fail("condition was not met") + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionPartsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionPartsTest.kt new file mode 100644 index 00000000000..585ba6e4bce --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/PromptMentionPartsTest.kt @@ -0,0 +1,161 @@ +package ai.kilocode.client.session.ui.prompt + +import junit.framework.TestCase +import kotlinx.coroutines.runBlocking +import java.nio.file.Path + +class PromptMentionPartsTest : TestCase() { + + fun `test promptMentions parses boundary mentions`() { + assertEquals( + listOf(Mention("src/Main.kt", 5, 17), Mention("README.md", 22, 32)), + promptMentions("read @src/Main.kt and @README.md"), + ) + assertEquals(listOf(Mention("src/x.kt", 4, 13)), promptMentions("see @src/x.kt")) + } + + fun `test promptMentions ignores embedded and bare markers`() { + assertTrue(promptMentions("read foo@src/Main.kt and @").isEmpty()) + } + + fun `test promptMentions handles whitespace and punctuation as token text`() { + assertEquals( + listOf(Mention("src/a.kt", 4, 13), Mention("src/b.kt,", 14, 24)), + promptMentions("see\t@src/a.kt\n@src/b.kt,"), + ) + } + + fun `test fileMentions drops reserved names and deduplicates`() { + assertEquals( + listOf(Mention("src/a.kt", 5, 14), Mention("src/b.kt", 38, 47)), + fileMentions("read @src/a.kt ${MentionAction.GIT_CHANGES.token} @src/a.kt @src/b.kt", setOf(MentionAction.GIT_CHANGES.name)), + ) + } + + fun `test mentionFileParts builds file part for tracked relative path`() { + val parts = mentionFileParts("read @src/Main.kt", setOf("src/Main.kt"), "/repo") + + assertEquals(1, parts.size) + val part = parts.single() + assertEquals("file", part.type) + assertEquals("text/plain", part.mime) + assertEquals(Path.of("/repo/src/Main.kt").toUri().toString(), part.url) + assertEquals("Main.kt", part.filename) + assertEquals("file", part.source?.type) + assertEquals("src/Main.kt", part.source?.path) + assertEquals("@src/Main.kt", part.source?.text?.value) + assertEquals(5.0, part.source?.text?.start) + assertEquals(17.0, part.source?.text?.end) + } + + fun `test mentionFileParts ignores untracked path`() { + assertTrue(mentionFileParts("read @src/Main.kt", setOf("src/Other.kt"), "/repo").isEmpty()) + } + + fun `test mentionFileParts ignores edited mention suffix`() { + assertTrue(mentionFileParts("read @src/Main.kt-extra", setOf("src/Main.kt"), "/repo").isEmpty()) + assertTrue(mentionFileParts("read @src/Main.kt.bak", setOf("src/Main.kt"), "/repo").isEmpty()) + } + + fun `test mentionFileParts ignores embedded mention text`() { + assertTrue(mentionFileParts("read foo@src/Main.kt", setOf("src/Main.kt"), "/repo").isEmpty()) + } + + fun `test mentionFileParts keeps absolute paths absolute`() { + val path = "/tmp/abs.txt" + val part = mentionFileParts("read @$path", setOf(path), "/repo").single() + + assertEquals(Path.of(path).toUri().toString(), part.url) + assertEquals("abs.txt", part.filename) + } + + fun `test mentionParts validates hand typed mention at send time`() = runBlocking { + val parts = mentionParts( + text = "read @src/Main.kt", + directory = "/repo", + reserved = setOf(MentionAction.GIT_CHANGES.name), + resolve = { path -> path == "src/Main.kt" }, + gitChanges = { null }, + ) + + val part = parts.single() + assertEquals(Path.of("/repo/src/Main.kt").toUri().toString(), part.url) + assertEquals("@src/Main.kt", part.source?.text?.value) + assertEquals(5.0, part.source?.text?.start) + } + + fun `test mentionParts drops unresolved file mentions`() = runBlocking { + assertTrue(mentionParts( + text = "read @src/Main.kt", + directory = "/repo", + reserved = setOf(MentionAction.GIT_CHANGES.name), + resolve = { false }, + gitChanges = { null }, + ).isEmpty()) + } + + fun `test mentionParts handles mixed resolved reserved and git changes`() = runBlocking { + val parts = mentionParts( + text = "read @src/Main.kt @missing.kt ${MentionAction.GIT_CHANGES.token}", + directory = "/repo", + reserved = setOf(MentionAction.GIT_CHANGES.name), + resolve = { path -> path == "src/Main.kt" }, + gitChanges = { "diff" }, + ) + + assertEquals(2, parts.size) + assertEquals("Main.kt", parts[0].filename) + assertEquals(MentionAction.GIT_CHANGES.filename, parts[1].filename) + assertEquals(MentionAction.GIT_CHANGES.uri, parts[1].source?.path) + } + + fun `test mentionParts skips blank git changes`() = runBlocking { + assertTrue(mentionParts( + text = "review ${MentionAction.GIT_CHANGES.token}", + directory = "/repo", + reserved = setOf(MentionAction.GIT_CHANGES.name), + resolve = { true }, + gitChanges = { " " }, + ).isEmpty()) + } + + fun `test mentionParts skips git lookup without token`() = runBlocking { + var called = false + + mentionParts( + text = "read @src/Main.kt", + directory = "/repo", + reserved = setOf(MentionAction.GIT_CHANGES.name), + resolve = { false }, + gitChanges = { + called = true + "diff" + }, + ) + + assertFalse(called) + } + + fun `test gitChangesPart builds encoded data part`() { + val part = gitChangesPart("review ${MentionAction.GIT_CHANGES.token}", "hello world+plus")!! + + assertEquals("file", part.type) + assertEquals("text/plain", part.mime) + assertEquals(MentionAction.GIT_CHANGES.filename, part.filename) + assertEquals("data:text/plain;charset=utf-8,hello%20world%2Bplus", part.url) + assertEquals("file", part.source?.type) + assertEquals(MentionAction.GIT_CHANGES.uri, part.source?.path) + assertNull(part.source?.clientName) + assertNull(part.source?.uri) + assertEquals(MentionAction.GIT_CHANGES.token, part.source?.text?.value) + assertEquals(7.0, part.source?.text?.start) + assertEquals(19.0, part.source?.text?.end) + } + + fun `test gitChangesPart ignores missing blank and non boundary matches`() { + assertNull(gitChangesPart("review ${MentionAction.GIT_CHANGES.token}", null)) + assertNull(gitChangesPart("review ${MentionAction.GIT_CHANGES.token}", " ")) + assertNull(gitChangesPart("review ${MentionAction.GIT_CHANGES.token}-foo", "diff")) + assertNull(gitChangesPart("review foo${MentionAction.GIT_CHANGES.token}", "diff")) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt index 8f62d8c0858..9165c7d3acf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/TextViewTest.kt @@ -1,8 +1,15 @@ package ai.kilocode.client.session.views +import ai.kilocode.client.session.model.FileAttachment +import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.model.Text import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.rpc.dto.MessageDto +import ai.kilocode.rpc.dto.MessageTimeDto +import ai.kilocode.rpc.dto.PartSourceDto +import ai.kilocode.rpc.dto.PartSourceTextDto +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.ide.CopyPasteManager import com.intellij.util.ui.JBUI import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -243,11 +250,170 @@ class TextViewTest : BasePlatformTestCase() { val urls = mutableListOf() val view = TextView(Text("p1"), openUrl = { urls.add(it) }) - view.md.simulateLink("https://kilocode.ai/docs") + view.simulateLink("https://kilocode.ai/docs") assertEquals(listOf("https://kilocode.ai/docs"), urls) } + fun `test linkifyMentions rewrites tracked token`() { + val out = linkifyMentions( + "read @src/a.kt", + listOf(PromptMention("@src/a.kt", "src/a.kt", 5, 14)), + ) + + assertEquals("read [@src/a.kt](src/a.kt)", out) + } + + fun `test linkifyMentions escapes text and encodes href`() { + val out = linkifyMentions( + "open @[a] file.kt", + listOf(PromptMention("@[a] file.kt", "[a] file.kt", 5, 17)), + ) + + assertEquals("open [@\\[a\\] file.kt]([a]%20file.kt)", out) + } + + fun `test linkifyMentions handles multiple mentions by offset`() { + val out = linkifyMentions( + "read @src/a.kt and @src/b.kt", + listOf( + PromptMention("@src/a.kt", "src/a.kt", 5, 14), + PromptMention("@src/b.kt", "src/b.kt", 19, 28), + ), + ) + + assertEquals("read [@src/a.kt](src/a.kt) and [@src/b.kt](src/b.kt)", out) + } + + fun `test linkifyMentions falls back to literal replacement when offset drifts`() { + val out = linkifyMentions( + "read @src/a.kt", + listOf(PromptMention("@src/a.kt", "src/a.kt", 0, 4)), + ) + + assertEquals("read [@src/a.kt](src/a.kt)", out) + } + + fun `test linkifyMentions fallback does not relink generated markdown`() { + val out = linkifyMentions( + "read @src/a.kt and @src/a.kt", + listOf( + PromptMention("@src/a.kt", "src/a.kt", 5, 14), + PromptMention("@src/a.kt", "src/a.kt", 0, 4), + ), + ) + + assertEquals("read [@src/a.kt](src/a.kt) and [@src/a.kt](src/a.kt)", out) + } + + fun `test linkifyMentions leaves text without mentions unchanged`() { + assertEquals("read @src/a.kt", linkifyMentions("read @src/a.kt", emptyList())) + } + + fun `test promptMentions extracts source backed text files`() { + val msg = Message(MessageDto("m1", "ses", "user", MessageTimeDto(0.0))) + msg.parts["keep"] = file("keep", "text/plain", "@src/a.kt", "src/a.kt", 0, 9) + msg.parts["image"] = file("image", "image/png", "@src/a.png", "src/a.png", 0, 10) + msg.parts["blank"] = file("blank", "text/plain", "@src/b.kt", "", 0, 9) + msg.parts["plain"] = FileAttachment("plain").also { it.mime = "text/plain" } + + val mentions = promptMentions(msg) + assertEquals(1, mentions.size) + assertEquals("@src/a.kt", mentions.single().token) + assertEquals("src/a.kt", mentions.single().path) + assertEquals(0, mentions.single().start) + assertEquals(9, mentions.single().end) + assertSame(msg.parts["keep"], mentions.single().attachment) + } + + fun `test prompt view renders mention as link`() { + val text = Text("p1").also { it.content.append("read @src/a.kt") } + val view = PromptView(text, mentions = listOf(PromptMention("@src/a.kt", "src/a.kt", 5, 14))) + + assertEquals("read [@src/a.kt](src/a.kt)", view.markdown()) + assertTrue(view.md.html().contains("href=\"src/a.kt\"")) + assertTrue(view.md.html().contains("@src/a.kt")) + } + + fun `test prompt view routes mention links to file and urls to browser`() { + val files = mutableListOf() + val urls = mutableListOf() + val text = Text("p1").also { it.content.append("read @src/a file.kt") } + val view = PromptView( + text, + openFile = { files.add(it) }, + openUrl = { urls.add(it) }, + mentions = listOf(PromptMention("@src/a file.kt", "src/a file.kt", 5, 19)), + ) + + view.simulateLink("src/a%20file.kt") + view.simulateLink("https://kilocode.ai/docs") + + assertEquals(listOf("src/a file.kt"), files) + assertEquals(listOf("https://kilocode.ai/docs"), urls) + } + + fun `test prompt view routes attachment backed mention link to attachment opener`() { + val opened = mutableListOf() + val item = file("f1", "text/plain", "@git-changes", "git-changes", 7, 19).also { + it.url = "data:text/plain;charset=utf-8,diff%20content" + it.filename = "git-changes.txt" + } + val text = Text("p1").also { it.content.append("review @git-changes") } + val view = PromptView( + text, + openFile = { error("should not open file") }, + openAttachment = { opened.add(it) }, + mentions = listOf(PromptMention("@git-changes", "git-changes", 7, 19, item)), + ) + + view.simulateLink("git-changes") + + assertEquals(listOf(item), opened) + } + + fun `test prompt view setMentions refreshes existing prompt`() { + val text = Text("p1").also { it.content.append("read @src/a.kt") } + val view = PromptView(text) + + view.setMentions(listOf(PromptMention("@src/a.kt", "src/a.kt", 5, 14))) + + assertEquals("read [@src/a.kt](src/a.kt)", view.markdown()) + } + + fun `test message view syncs prompt mentions from hidden part`() { + val msg = Message(MessageDto("m1", "ses", "user", MessageTimeDto(0.0))) + val view = MessageView(msg, openFile = {}) + val text = Text("p1").also { it.content.append("read @src/a.kt") } + msg.parts["p1"] = text + view.upsertPart(text) + + assertEquals("read @src/a.kt", (view.part("p1") as PromptView).markdown()) + + val mention = file("f1", "text/plain", "@src/a.kt", "src/a.kt", 5, 14) + msg.parts["f1"] = mention + view.upsertPart(mention) + + assertNull(view.part("f1")) + assertEquals("read [@src/a.kt](src/a.kt)", (view.part("p1") as PromptView).markdown()) + } + + fun `test prompt view colors links with metadata color`() { + val style = SessionEditorStyle.current() + val color = style.editorScheme.getAttributes(DefaultLanguageHighlighterColors.METADATA)?.foregroundColor + val view = PromptView(Text("p1")) + val before = view.md.linkColor + + view.applyStyle(style) + + assertEquals(color ?: before, view.md.linkColor) + } + + private fun file(id: String, mime: String, token: String, path: String, start: Int, end: Int) = FileAttachment(id).also { + it.mime = mime + it.source = PartSourceDto("file", PartSourceTextDto(token, start.toDouble(), end.toDouble()), path = path) + } + private class TrackingRepaintManager(private val watched: JComponent) : RepaintManager() { var dirty = 0 var invalid = 0 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index eac63e23a1c..a7053469f27 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -84,7 +84,9 @@ class FakeSessionRpcApi : KiloSessionRpcApi { var enhanced = "Enhanced prompt" var enhanceGate: CompletableDeferred? = null var enhanceThrows: Exception? = null + var commandThrows: Exception? = null val prompts = mutableListOf>() + val commands = mutableListOf() val attachmentParts = mutableListOf() val aborts = mutableListOf>() val compacts = mutableListOf>() @@ -106,6 +108,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi { data class CloudCall(val directory: String, val cursor: String?, val limit: Int, val gitUrl: String?) data class AttachmentCall(val id: String, val directory: String, val messageId: String, val partId: String, val attachmentKey: String?) + data class CommandCall(val id: String, val directory: String, val command: String, val arguments: String, val prompt: PromptDto) // --- Implementation --- @@ -195,6 +198,12 @@ class FakeSessionRpcApi : KiloSessionRpcApi { prompts.add(Triple(id, directory, prompt)) } + override suspend fun command(id: String, directory: String, command: String, arguments: String, prompt: PromptDto) { + assertNotEdt("command") + commandThrows?.let { throw it } + commands.add(CommandCall(id, directory, command, arguments, prompt)) + } + override suspend fun abort(id: String, directory: String) { assertNotEdt("abort") aborts.add(id to directory) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt index bfdb5daece6..4d6c6aecf9e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.testing import ai.kilocode.rpc.KiloWorkspaceRpcApi import ai.kilocode.rpc.dto.ConfigTargetDto +import ai.kilocode.rpc.dto.FileSearchResultDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto import ai.kilocode.rpc.dto.ModelsWorkspaceDto @@ -27,6 +28,10 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { var models = ModelsWorkspaceDto() var modelsGate: CompletableDeferred? = null var fileMatches = emptyList() + var fileResolver: ((String) -> List)? = null + var searchResult = FileSearchResultDto() + var search: ((String) -> FileSearchResultDto)? = null + var gitChanges: String? = null var openResult = true var localConfigPath = "/test/.kilo/kilo.jsonc" var globalConfigPath = "/config/kilo.jsonc" @@ -35,6 +40,7 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { var localConfigExists = true var globalConfigExists = true val fileCalls = mutableListOf>() + val searchQueries = mutableListOf() val opened = mutableListOf() val localConfigs = mutableListOf() var globalConfigs = 0 @@ -67,7 +73,18 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { override suspend fun files(directory: String, path: String): List { assertNotEdt("files") fileCalls.add(directory to path) - return fileMatches + return fileResolver?.invoke(path) ?: fileMatches + } + + override suspend fun searchFiles(directory: String, query: String, limit: Int): FileSearchResultDto { + assertNotEdt("searchFiles") + searchQueries.add(query) + return search?.invoke(query) ?: searchResult + } + + override suspend fun gitChanges(directory: String): String? { + assertNotEdt("gitChanges") + return gitChanges } override suspend fun openFile(path: String): Boolean { diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index a1cdd86dc7a..dc2d2c7f4e6 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.1-rc.11 +kilo.jetbrains.version=7.0.1-rc.13 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 40dd7d8b8fd..77ce75e322d 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.3.46", + "version": "7.3.54", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-jetbrains/script/build.ts b/packages/kilo-jetbrains/script/build.ts index 90e9518c608..666040cc6cb 100644 --- a/packages/kilo-jetbrains/script/build.ts +++ b/packages/kilo-jetbrains/script/build.ts @@ -12,9 +12,8 @@ * 1. Builds CLI binaries (or uses prebuilt ones from dist/). * Local: builds only current platform (--single). * Production: builds all platforms. - * 2. Copies them into backend/build/generated/cli/cli/{os}/kilo[.exe] - * plus models-snapshot.json sidecars. - * so they end up inside the backend jar at /cli/{os}/kilo. + * 2. Copies them and the Kilo sandbox mutation worker into backend/build/generated/cli/cli/{os}/ + * so they end up inside the backend jar at /cli/{os}/. * 3. Invokes Gradle to build the plugin. */ @@ -54,17 +53,17 @@ function distBinPath(os: string, exe: string): string { return join(distDir, `@kilocode/cli-${os}`, "bin", exe) } -function distSnapshotPath(os: string): string { - return join(distDir, `@kilocode/cli-${os}`, "bin", "models-snapshot.json") +function hasArtifacts(os: string, exe: string): boolean { + return existsSync(distBinPath(os, exe)) && existsSync(distBinPath(os, "kilo-sandbox-mutation-worker.js")) } function hasDist(): boolean { if (production) { - return platforms.every((p) => existsSync(distBinPath(p.os, p.exe)) && existsSync(distSnapshotPath(p.os))) + return platforms.every((p) => hasArtifacts(p.os, p.exe)) } const tag = localPlatformTag() const local = platforms.find((p) => p.os === tag) - return local ? existsSync(distBinPath(local.os, local.exe)) && existsSync(distSnapshotPath(local.os)) : false + return local ? hasArtifacts(local.os, local.exe) : false } async function prepareCli() { @@ -90,8 +89,8 @@ async function prepareCli() { let copied = 0 for (const p of platforms) { const src = distBinPath(p.os, p.exe) - const snapshot = distSnapshotPath(p.os) - if (!existsSync(src) || !existsSync(snapshot)) { + const worker = distBinPath(p.os, "kilo-sandbox-mutation-worker.js") + if (!existsSync(src) || !existsSync(worker)) { missing.push(p.os) continue } @@ -100,10 +99,10 @@ async function prepareCli() { mkdirSync(dir, { recursive: true }) const dest = join(dir, p.exe) cpSync(src, dest) - cpSync(snapshot, join(dir, "models-snapshot.json")) + cpSync(worker, join(dir, "kilo-sandbox-mutation-worker.js")) chmodSync(dest, 0o755) copied++ - log(`Copied ${relative(root, src)} -> ${relative(root, dest)}`) + log(`Copied ${relative(root, src)} and Kilo sandbox mutation worker -> ${relative(root, dir)}`) } if (copied === 0) { diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index fe53f8c35ee..feb68cd5c4e 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -78,6 +78,9 @@ interface KiloSessionRpcApi : RemoteApi { /** Send a prompt to a session (fire-and-forget). */ suspend fun prompt(id: String, directory: String, prompt: PromptDto) + /** Run a configured slash command/workflow in a session. */ + suspend fun command(id: String, directory: String, command: String, arguments: String, prompt: PromptDto) + /** Abort ongoing processing for a session. */ suspend fun abort(id: String, directory: String) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt index aea6c43e36a..3c8c0d00da0 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt @@ -1,6 +1,7 @@ package ai.kilocode.rpc import ai.kilocode.rpc.dto.ConfigTargetDto +import ai.kilocode.rpc.dto.FileSearchResultDto import ai.kilocode.rpc.dto.KiloWorkspaceStateDto import ai.kilocode.rpc.dto.ModelsWorkspaceDto import ai.kilocode.rpc.dto.WorkspaceFileDto @@ -46,6 +47,12 @@ interface KiloWorkspaceRpcApi : RemoteApi { /** Resolve [path] to matching files, scoped primarily to [directory]. */ suspend fun files(directory: String, path: String): List + /** Fuzzy file/folder search via the backend IDE index. */ + suspend fun searchFiles(directory: String, query: String, limit: Int = 50): FileSearchResultDto + + /** Current uncommitted git changes as a unified diff for @git-changes mentions. */ + suspend fun gitChanges(directory: String): String? + /** Open an absolute backend file path in the IDE. */ suspend fun openFile(path: String): Boolean diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index 21bf20897b0..623915fc529 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -75,6 +75,26 @@ data class PartDto( val mime: String? = null, val url: String? = null, val filename: String? = null, + val synthetic: Boolean? = null, + val source: PartSourceDto? = null, +) + +@Serializable +data class PartSourceDto( + val type: String, + val text: PartSourceTextDto, + val path: String? = null, + val clientName: String? = null, + val uri: String? = null, + val name: String? = null, + val kind: Int? = null, +) + +@Serializable +data class PartSourceTextDto( + val value: String, + val start: Double, + val end: Double, ) @Serializable @@ -103,6 +123,7 @@ data class PromptPartDto( val mime: String? = null, val url: String? = null, val filename: String? = null, + val source: PartSourceDto? = null, ) // --- Streaming Events --- diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt index 6045f5c130f..c17933c6594 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt @@ -11,10 +11,12 @@ data class ModelDto( val temperature: Boolean = false, val toolCall: Boolean = false, val free: Boolean = false, + val byok: Boolean = false, val status: String? = null, val recommendedIndex: Double? = null, val variants: List = emptyList(), val limit: ModelLimitDto? = null, + val mayTrainOnYourPrompts: Boolean = false, ) @Serializable diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorkspaceFileDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorkspaceFileDto.kt index 1f3a1ac5b7a..8364b1b1918 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorkspaceFileDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/WorkspaceFileDto.kt @@ -8,3 +8,10 @@ data class WorkspaceFileDto( val name: String, val directory: Boolean = false, ) + +@Serializable +data class FileSearchResultDto( + val indexing: Boolean = false, + val files: List = emptyList(), + val git: Boolean = false, +) diff --git a/packages/kilo-sandbox/package.json b/packages/kilo-sandbox/package.json new file mode 100644 index 00000000000..2bf4ea60d11 --- /dev/null +++ b/packages/kilo-sandbox/package.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@kilocode/sandbox", + "version": "7.3.52", + "type": "module", + "license": "MIT", + "private": true, + "description": "OS-neutral sandbox profiles and launch preparation for Kilo Code", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "scripts": { + "typecheck": "tsgo --noEmit", + "test": "bun test --timeout 30000", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" + }, + "dependencies": { + "effect": "catalog:" + }, + "devDependencies": { + "@effect/platform-node": "catalog:", + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + }, + "peerDependencies": {} +} diff --git a/packages/kilo-sandbox/src/backend.ts b/packages/kilo-sandbox/src/backend.ts new file mode 100644 index 00000000000..7d541b46ea1 --- /dev/null +++ b/packages/kilo-sandbox/src/backend.ts @@ -0,0 +1,119 @@ +import { Effect, PlatformError, Scope } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { bubblewrap } from "./bubblewrap" +import { current } from "./context" +import { assertProcessNetwork, networkEnvironment } from "./network" +import type { Profile } from "./profile" +import { seatbelt } from "./seatbelt" + +export interface Launch { + readonly command: string + readonly args: ReadonlyArray + readonly cwd?: string | undefined + readonly environment?: Readonly> | undefined + readonly shell?: boolean | string | undefined +} + +export interface Support { + readonly available: boolean + readonly reason?: string | undefined +} + +export interface Backend { + readonly support: (network?: Profile["network"]) => Support + readonly prepare: ( + profile: Profile, + launch: Launch, + ) => Effect.Effect +} + +function unavailable(reason: string): Backend { + return { + support: () => ({ available: false, reason }), + prepare: (_profile, launch) => Effect.succeed(launch), + } +} + +function select(): Backend { + switch (process.platform) { + case "darwin": + return seatbelt + case "linux": + return bubblewrap + case "win32": + return unavailable("The Windows sandbox backend is not available") + default: + return unavailable("No sandbox backend is available for this operating system") + } +} + +const backend = select() + +function environment(profile: Profile, launch: Launch) { + const source = { ...launch.environment, ...profile.environment.set } + const denied = new Set(profile.environment.deny) + const entries = Object.entries(source).filter( + (entry): entry is [string, string] => entry[1] !== undefined && !denied.has(entry[0]), + ) + return networkEnvironment(profile, Object.fromEntries(entries)) +} + +export function prepare(launch: Launch) { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return launch + const next = { ...launch, environment: environment(profile, launch) } + yield* assertProcessNetwork(profile, launch.command) + if (!backend.support().available) return next + return yield* backend.prepare(profile, next) + }) +} + +function unsupported(command: string, method: string, support: Support) { + return PlatformError.systemError({ + _tag: "PermissionDenied", + module: "Sandbox", + method, + pathOrDescriptor: command, + description: support.reason ?? "The process sandbox backend is unavailable", + }) +} + +export function confine(profile: Profile, launch: Launch) { + return Effect.gen(function* () { + const next = { ...launch, environment: environment(profile, launch) } + yield* assertProcessNetwork(profile, launch.command) + const support = backend.support(profile.network) + if (!support.available) return yield* Effect.fail(unsupported(launch.command, "confine", support)) + return yield* backend.prepare(profile, next) + }) +} + +export function prepareCommand( + command: ChildProcess.StandardCommand, + cwd: string | undefined, + env: Readonly> | undefined, +) { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return command + const launch = yield* confine(profile, { + command: command.command, + args: command.args, + cwd, + environment: env, + shell: command.options.shell, + }) + return ChildProcess.make(launch.command, launch.args, { + ...command.options, + cwd: launch.cwd, + env: launch.environment, + extendEnv: false, + shell: false, + }) + }) +} + +export function backendSupport(network?: Profile["network"]) { + return backend.support(network) +} diff --git a/packages/kilo-sandbox/src/bubblewrap.ts b/packages/kilo-sandbox/src/bubblewrap.ts new file mode 100644 index 00000000000..1440185c1cc --- /dev/null +++ b/packages/kilo-sandbox/src/bubblewrap.ts @@ -0,0 +1,272 @@ +import { spawnSync } from "node:child_process" +import { createHash } from "node:crypto" +import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs" +import path from "node:path" +import { Effect, PlatformError } from "effect" +import type { Backend, Launch, Support } from "./backend" +import type { PathRule, Profile } from "./profile" + +declare const KILO_BWRAP_SHA256: string | undefined + +const system = "/usr/bin/bwrap" + +function quote(value: string) { + return `'${value.replaceAll("'", `'\\''`)}'` +} + +function command(launch: Launch) { + if (!launch.shell) return [launch.command, ...launch.args] + const shell = typeof launch.shell === "string" ? launch.shell : "/bin/sh" + return [shell, "-c", [launch.command, ...launch.args.map(quote)].join(" ")] +} + +function exists(rule: PathRule) { + if (!existsSync(rule.path)) return false + const entry = statSync(rule.path) + if (rule.kind === "literal") return entry.isFile() + return entry.isDirectory() +} + +function writable(profile: Profile) { + const seen = new Set() + return profile.filesystem.allowWrite + .filter(exists) + .filter((rule) => { + if (seen.has(rule.path)) return false + seen.add(rule.path) + return true + }) + .sort((a, b) => a.path.length - b.path.length) +} + +function beneath(root: string, target: string) { + const relative = path.relative(root, target) + return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) +} + +function unescape(value: string) { + return value.replace(/\\([0-7]{3})/g, (_match, code: string) => String.fromCharCode(Number.parseInt(code, 8))) +} + +export function parseMountinfo(content: string) { + return content + .split("\n") + .filter(Boolean) + .map((line) => { + const value = line.split(" ")[4] + if (!value) throw new Error("Could not parse /proc/self/mountinfo") + return unescape(value) + }) +} + +function mountpoints() { + return parseMountinfo(readFileSync("/proc/self/mountinfo", "utf8")) +} + +function validate(allow: ReadonlyArray, executable: string, mounts: ReadonlyArray) { + if (allow.some((rule) => beneath(rule.path, executable))) { + throw new Error(`Bubblewrap executable is writable by the sandbox profile: ${executable}`) + } + + for (const rule of allow) { + if (rule.kind !== "subtree") continue + const nested = mounts.find((mount) => mount !== rule.path && beneath(rule.path, mount)) + if (nested) throw new Error(`Writable root contains a nested mount point: ${nested}`) + } +} + +function scan(root: string, names: ReadonlySet, found: Set) { + if (names.has(path.basename(root))) { + found.add(root) + return + } + if (!statSync(root).isDirectory()) return + + const pending = [root] + while (pending.length > 0) { + const dir = pending.pop() + if (!dir) continue + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const target = path.join(dir, entry.name) + if (names.has(entry.name)) { + found.add(target) + continue + } + if (entry.isDirectory()) pending.push(target) + } + } +} + +function protectedPaths(profile: Profile, allow: ReadonlyArray) { + const found = new Set(profile.filesystem.denyWrite.filter((rule) => existsSync(rule.path)).map((rule) => rule.path)) + if (profile.filesystem.denyNames.length === 0) return [...found] + + const names = new Set(profile.filesystem.denyNames) + for (const rule of allow) { + if (rule.kind === "subtree") scan(rule.path, names, found) + } + return [...found].sort((a, b) => a.length - b.length) +} + +export function generate( + profile: Profile, + launch: Launch, + executable: string, + mounts = process.platform === "linux" ? mountpoints() : [], +): Launch { + const allow = writable(profile) + validate(allow, executable, mounts) + const args = [ + "--unshare-user", + "--disable-userns", + "--unshare-pid", + ...(profile.network.mode === "deny" ? ["--unshare-net"] : []), + "--die-with-parent", + "--new-session", + "--ro-bind", + "/", + "/", + "--dev", + "/dev", + ] + + for (const rule of allow) args.push("--bind", rule.path, rule.path) + for (const target of protectedPaths(profile, allow)) args.push("--ro-bind", target, target) + args.push("--proc", "/proc") + if (launch.cwd) args.push("--chdir", launch.cwd) + args.push("--", ...command(launch)) + + return { + ...launch, + command: executable, + args, + } +} + +function bundled() { + return path.join(path.dirname(process.execPath), "bwrap") +} + +function digest() { + return typeof KILO_BWRAP_SHA256 === "undefined" ? undefined : KILO_BWRAP_SHA256 +} + +function resolve(executable: string, expected?: string) { + try { + if (!path.isAbsolute(executable)) return + const target = realpathSync.native(executable) + const entry = statSync(target) + if (!entry.isFile() || (entry.mode & 0o6000) !== 0) return + if (expected && createHash("sha256").update(readFileSync(target)).digest("hex") !== expected) return + return target + } catch { + return + } +} + +function probe(executable: string, network = false) { + const result = spawnSync( + executable, + [ + "--unshare-user", + "--disable-userns", + "--unshare-pid", + ...(network ? ["--unshare-net"] : []), + "--die-with-parent", + "--new-session", + "--ro-bind", + "/", + "/", + "--dev", + "/dev", + "--proc", + "/proc", + "--", + executable, + "--version", + ], + { encoding: "utf8", timeout: 5_000 }, + ) + if (result.status === 0) return undefined + const detail = result.error?.message ?? (result.stderr.trim() || `exited with status ${result.status}`) + const capability = network ? "Linux network sandbox" : "Linux sandbox" + return `${executable} could not create the ${capability}: ${detail}` +} + +interface Selection { + readonly executable: string | undefined + readonly support: Support + network: Support | undefined +} + +function select(): Selection { + const override = process.env.KILO_BWRAP_PATH + const candidates = override + ? [{ executable: override }] + : [{ executable: system }, { executable: bundled(), expected: digest() }] + const failures: Array = [] + + for (const candidate of candidates) { + const executable = resolve(candidate.executable, candidate.expected) + if (!executable) continue + const failure = probe(executable) + if (!failure) return { executable, support: { available: true } satisfies Support, network: undefined } + failures.push(failure) + } + + return { + executable: undefined, + support: { + available: false, + reason: failures.at(-1) ?? "No usable Bubblewrap executable is available", + } satisfies Support, + network: undefined, + } +} + +let selected: Selection | undefined + +function selection(): Selection { + if (selected) return selected + selected = + process.platform === "linux" + ? select() + : { + executable: undefined, + support: { available: false, reason: "Bubblewrap requires Linux" } satisfies Support, + network: undefined, + } + return selected +} + +function support(network?: Profile["network"]): Support { + const selected = selection() + if (!selected.support.available || network?.mode !== "deny" || !selected.executable) return selected.support + if (selected.network) return selected.network + const failure = probe(selected.executable, true) + selected.network = failure ? { available: false, reason: failure } : { available: true } + return selected.network +} + +function setup(cause: unknown, launch: Launch) { + return PlatformError.systemError({ + _tag: "PermissionDenied", + module: "Sandbox", + method: "prepareCommand", + pathOrDescriptor: launch.command, + description: cause instanceof Error ? cause.message : "Could not construct the Linux sandbox", + cause, + }) +} + +export const bubblewrap: Backend = { + support, + prepare: (profile, launch) => + Effect.try({ + try: () => { + const selected = selection() + return selected.executable ? generate(profile, launch, selected.executable) : launch + }, + catch: (cause) => setup(cause, launch), + }), +} diff --git a/packages/kilo-sandbox/src/context.ts b/packages/kilo-sandbox/src/context.ts new file mode 100644 index 00000000000..71609d12a97 --- /dev/null +++ b/packages/kilo-sandbox/src/context.ts @@ -0,0 +1,75 @@ +import { Context, Effect, PlatformError } from "effect" +import { canonicalize, canonicalizeEntry, matches, normalize } from "./path" +import type { Profile } from "./profile" + +export const CurrentProfile = Context.Reference("@kilocode/sandbox/CurrentProfile", { + defaultValue: () => undefined, +}) + +export const current: Effect.Effect = Effect.gen(function* () { + return yield* CurrentProfile +}) + +export const enabled: Effect.Effect = Effect.map(current, (profile) => profile !== undefined) + +export function run( + profile: Profile, + effect: Effect.Effect, +): Effect.Effect { + return Effect.gen(function* () { + const value = yield* normalize(profile) + return yield* effect.pipe(Effect.provideService(CurrentProfile, value)) + }) +} + +export function unrestricted(effect: Effect.Effect) { + return effect.pipe(Effect.provideService(CurrentProfile, undefined)) +} + +function denied(path: string, method: string) { + return PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method, + pathOrDescriptor: path, + description: "Sandbox denied write access", + }) +} + +function assertTarget( + path: string, + method: string, + resolve: (path: string) => Effect.Effect, +): Effect.Effect { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return + const target = yield* resolve(path) + const names = + process.platform === "win32" + ? profile.filesystem.denyNames.map((name) => name.toLowerCase()) + : profile.filesystem.denyNames + const parts = target.split(/[\\/]/).map((part) => (process.platform === "win32" ? part.toLowerCase() : part)) + if ( + profile.filesystem.denyWrite.some((rule) => matches(rule, target)) || + parts.some((part) => names.includes(part)) + ) { + yield* Effect.fail(denied(path, method)) + } + if (!profile.filesystem.allowWrite.some((rule) => matches(rule, target))) { + yield* Effect.fail(denied(path, method)) + } + }) +} + +export function assertPath(path: string, method: string): Effect.Effect { + return assertTarget(path, method, canonicalize) +} + +export function assertEntry(path: string, method: string): Effect.Effect { + return assertTarget(path, method, canonicalizeEntry) +} + +export function assertWrite(path: string): Effect.Effect { + return assertPath(path, "assertWrite") +} diff --git a/packages/kilo-sandbox/src/filesystem.ts b/packages/kilo-sandbox/src/filesystem.ts new file mode 100644 index 00000000000..57c50f30b22 --- /dev/null +++ b/packages/kilo-sandbox/src/filesystem.ts @@ -0,0 +1,247 @@ +import { dirname } from "node:path" +import { tmpdir } from "node:os" +import { Effect, FileSystem, Layer, PlatformError, Scope, Sink } from "effect" +import { assertEntry, assertPath, current } from "./context" +import { currentRunner } from "./mutation" +import { date, type Request } from "./mutation-protocol" +import type { Profile } from "./profile" + +interface TempOptions { + readonly directory?: string | undefined + readonly prefix?: string | undefined + readonly suffix?: string | undefined +} + +export function ensureDirectory(fs: FileSystem.FileSystem, path: string) { + return fs.makeDirectory(path, { recursive: true }).pipe( + Effect.catchIf( + (err) => err.reason._tag === "AlreadyExists", + () => Effect.void, + ), + ) +} + +function execute(profile: Profile, request: Request, effect: Effect.Effect) { + return Effect.gen(function* () { + yield* effect + yield* (yield* currentRunner)(profile, request) + }) +} + +function openDenied(path: string) { + return PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method: "open", + pathOrDescriptor: path, + description: "Writable file handles are unavailable while the sandbox is enabled", + }) +} + +function tempOptions(profile: Profile, options: TempOptions | undefined) { + const directory = options?.directory ?? profile.filesystem.temporaryDirectory ?? tmpdir() + return { directory, options: { ...options, directory } } +} + +function temporary( + method: "makeTempDirectory" | "makeTempFile", + options: TempOptions | undefined, + create: (options?: TempOptions) => Effect.Effect, +) { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* create(options) + const temp = tempOptions(profile, options) + yield* assertPath(temp.directory, method) + const result = yield* (yield* currentRunner)(profile, { op: method, options: temp.options }) + if (result !== undefined) return result + return yield* Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "Sandbox", + method, + pathOrDescriptor: temp.directory, + description: "Filesystem worker returned no temporary path", + }), + ) + }) +} + +function scoped( + method: "makeTempDirectory" | "makeTempFile", + options: TempOptions | undefined, + create: (options?: TempOptions) => Effect.Effect, +) { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* create(options) + const temp = tempOptions(profile, options) + yield* assertPath(temp.directory, method) + const runner = yield* currentRunner + return yield* Effect.acquireRelease( + runner(profile, { op: method, options: temp.options }).pipe( + Effect.flatMap((result) => + result === undefined + ? Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "Sandbox", + method, + pathOrDescriptor: temp.directory, + description: "Filesystem worker returned no temporary path", + }), + ) + : Effect.succeed(result), + ), + ), + (path) => + runner(profile, { + op: "remove", + path: method === "makeTempFile" ? dirname(path) : path, + options: { recursive: true }, + }).pipe(Effect.orDie), + ) + }) +} + +export function decorateFileSystem(fs: FileSystem.FileSystem): FileSystem.FileSystem { + return FileSystem.FileSystem.of({ + ...fs, + chmod: (path, mode) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.chmod(path, mode) + return yield* execute(profile, { op: "chmod", path, mode }, assertPath(path, "chmod")) + }), + chown: (path, uid, gid) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.chown(path, uid, gid) + return yield* execute(profile, { op: "chown", path, uid, gid }, assertPath(path, "chown")) + }), + copy: (from, to, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.copy(from, to, options) + return yield* execute(profile, { op: "copy", from, to, options }, assertPath(to, "copy")) + }), + copyFile: (from, to) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.copyFile(from, to) + return yield* execute(profile, { op: "copyFile", from, to }, assertPath(to, "copyFile")) + }), + link: (from, to) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.link(from, to) + const check = assertPath(from, "link").pipe(Effect.andThen(assertPath(to, "link"))) + return yield* execute(profile, { op: "link", from, to }, check) + }), + makeDirectory: (path, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.makeDirectory(path, options) + return yield* execute(profile, { op: "makeDirectory", path, options }, assertPath(path, "makeDirectory")) + }), + makeTempDirectory: (options) => temporary("makeTempDirectory", options, fs.makeTempDirectory), + makeTempDirectoryScoped: (options) => scoped("makeTempDirectory", options, fs.makeTempDirectoryScoped), + makeTempFile: (options) => temporary("makeTempFile", options, fs.makeTempFile), + makeTempFileScoped: (options) => scoped("makeTempFile", options, fs.makeTempFileScoped), + open: (path, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile || (options?.flag ?? "r") === "r") return yield* fs.open(path, options) + yield* assertPath(path, "open") + return yield* Effect.fail(openDenied(path)) + }), + remove: (path, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.remove(path, options) + return yield* execute(profile, { op: "remove", path, options }, assertEntry(path, "remove")) + }), + rename: (from, to) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.rename(from, to) + const check = assertEntry(from, "rename").pipe(Effect.andThen(assertEntry(to, "rename"))) + return yield* execute(profile, { op: "rename", from, to }, check) + }), + sink: (path, options) => + Sink.unwrap( + Effect.gen(function* () { + const profile = yield* current + if (!profile) return fs.sink(path, options) + const runner = yield* currentRunner + const collect = Sink.foldArray( + () => [] as Uint8Array[], + () => true, + (chunks, input: ReadonlyArray) => Effect.sync(() => [...chunks, ...input]), + ) + return Sink.mapEffect(collect, (chunks) => { + const request: Request = { + op: "writeFile", + path, + data: Buffer.concat(chunks).toString("base64"), + options, + } + return assertPath(path, "sink").pipe(Effect.andThen(runner(profile, request)), Effect.asVoid) + }) + }), + ), + symlink: (from, to) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.symlink(from, to) + return yield* execute(profile, { op: "symlink", from, to }, assertPath(to, "symlink")) + }), + truncate: (path, length) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.truncate(path, length) + return yield* execute( + profile, + { op: "truncate", path, length: length === undefined ? undefined : Number(length) }, + assertPath(path, "truncate"), + ) + }), + utimes: (path, atime, mtime) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.utimes(path, atime, mtime) + return yield* execute( + profile, + { op: "utimes", path, atime: date(atime), mtime: date(mtime) }, + assertPath(path, "utimes"), + ) + }), + writeFile: (path, data, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.writeFile(path, data, options) + return yield* execute( + profile, + { op: "writeFile", path, data: Buffer.from(data).toString("base64"), options }, + assertPath(path, "writeFile"), + ) + }), + writeFileString: (path, data, options) => + Effect.gen(function* () { + const profile = yield* current + if (!profile) return yield* fs.writeFileString(path, data, options) + return yield* execute( + profile, + { op: "writeFileString", path, data, options }, + assertPath(path, "writeFileString"), + ) + }), + }) +} + +export const layer: Layer.Layer = Layer.effect( + FileSystem.FileSystem, + Effect.gen(function* () { + return decorateFileSystem(yield* FileSystem.FileSystem) + }), +) diff --git a/packages/kilo-sandbox/src/index.ts b/packages/kilo-sandbox/src/index.ts new file mode 100644 index 00000000000..cf942e3e7a9 --- /dev/null +++ b/packages/kilo-sandbox/src/index.ts @@ -0,0 +1,7 @@ +export type { Profile } from "./profile" +export { assertWrite, enabled, run, unrestricted } from "./context" +export { decorateFileSystem, ensureDirectory } from "./filesystem" +export { assertNetwork, decorateHttpClient, httpLayer as networkHttpLayer } from "./network" +export { batchMutations, mutate, withRunner, type Runner as MutationRunner } from "./mutation" +export type { Request as MutationRequest } from "./mutation-protocol" +export { backendSupport, prepareCommand } from "./backend" diff --git a/packages/kilo-sandbox/src/kilo-sandbox-mutation-worker.ts b/packages/kilo-sandbox/src/kilo-sandbox-mutation-worker.ts new file mode 100644 index 00000000000..3771daa1002 --- /dev/null +++ b/packages/kilo-sandbox/src/kilo-sandbox-mutation-worker.ts @@ -0,0 +1,140 @@ +import { randomBytes } from "node:crypto" +import * as fs from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { isRequest, type Failure, type Operation, type Request, type Response, type Time } from "./mutation-protocol" + +function time(value: Time) { + return value.type === "date" ? new Date(value.value) : value.value +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function field(value: unknown, key: string) { + if (!isObject(value)) return undefined + return value[key] +} + +function isMutationFailure(value: unknown): value is { readonly error: Failure } { + return isObject(value) && isObject(value.error) && typeof value.error.message === "string" +} + +function failure(cause: unknown, operation?: Operation["op"]): Failure { + const error = cause instanceof Error ? cause : new Error(String(cause)) + const code = field(cause, "code") + const errno = field(cause, "errno") + const syscall = field(cause, "syscall") + const path = field(cause, "path") + const dest = field(cause, "dest") + return { + name: error.name, + message: error.message, + code: typeof code === "string" ? code : undefined, + errno: typeof errno === "number" ? errno : undefined, + syscall: typeof syscall === "string" ? syscall : undefined, + path: typeof path === "string" ? path : undefined, + dest: typeof dest === "string" ? dest : undefined, + operation, + } +} + +async function temporary(request: Extract) { + const prefix = request.options?.prefix ?? "" + const directory = request.options?.directory ? join(request.options.directory, ".") : tmpdir() + return fs.mkdtemp(prefix ? join(directory, prefix) : directory + "/") +} + +async function mutate(request: Operation): Promise { + switch (request.op) { + case "chmod": + await fs.chmod(request.path, request.mode) + return undefined + case "chown": + await fs.chown(request.path, request.uid, request.gid) + return undefined + case "copy": + await fs.cp(request.from, request.to, { + force: request.options?.overwrite ?? false, + preserveTimestamps: request.options?.preserveTimestamps ?? false, + recursive: true, + }) + return undefined + case "copyFile": + await fs.copyFile(request.from, request.to) + return undefined + case "link": + await fs.link(request.from, request.to) + return undefined + case "makeDirectory": + await fs.mkdir(request.path, { + recursive: request.options?.recursive ?? false, + mode: request.options?.mode, + }) + return undefined + case "makeTempDirectory": + return temporary(request) + case "makeTempFile": { + const directory = await temporary(request) + const name = join(directory, randomBytes(6).toString("hex") + (request.options?.suffix ?? "")) + await fs.writeFile(name, new Uint8Array(0)) + return name + } + case "remove": + await fs.rm(request.path, { + recursive: request.options?.recursive ?? false, + force: request.options?.force ?? false, + }) + return undefined + case "rename": + await fs.rename(request.from, request.to) + return undefined + case "symlink": + await fs.symlink(request.from, request.to) + return undefined + case "truncate": + await fs.truncate(request.path, request.length) + return undefined + case "utimes": + await fs.utimes(request.path, time(request.atime), time(request.mtime)) + return undefined + case "writeFile": + await fs.writeFile(request.path, Buffer.from(request.data, "base64"), request.options) + return undefined + case "writeFileString": + await fs.writeFile(request.path, request.data, request.options) + return undefined + } + throw new TypeError("Unsupported filesystem mutation") +} + +async function read() { + const chunks: Buffer[] = [] + for await (const chunk of process.stdin) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + const value: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8")) + if (!isRequest(value)) throw new TypeError("Invalid filesystem mutation request") + return value +} + +function apply(operation: Operation) { + return mutate(operation).catch((cause) => Promise.reject({ error: failure(cause, operation.op) })) +} + +async function execute(request: Request) { + if (request.op !== "batch") return apply(request) + for (const operation of request.operations) await apply(operation) + return undefined +} + +const response: Response = await read() + .then(execute) + .then( + (value) => ({ ok: true, value }), + (cause) => ({ ok: false, error: isMutationFailure(cause) ? cause.error : failure(cause) }), + ) +await new Promise((resolve, reject) => { + process.stdout.write(JSON.stringify(response), (error) => (error ? reject(error) : resolve())) +}) diff --git a/packages/kilo-sandbox/src/mutation-protocol.ts b/packages/kilo-sandbox/src/mutation-protocol.ts new file mode 100644 index 00000000000..a3cfd54b59f --- /dev/null +++ b/packages/kilo-sandbox/src/mutation-protocol.ts @@ -0,0 +1,124 @@ +import type { OpenFlag } from "effect/FileSystem" + +export interface Options { + readonly flag?: OpenFlag | undefined + readonly mode?: number | undefined + readonly recursive?: boolean | undefined + readonly force?: boolean | undefined + readonly overwrite?: boolean | undefined + readonly preserveTimestamps?: boolean | undefined + readonly directory?: string | undefined + readonly prefix?: string | undefined + readonly suffix?: string | undefined +} + +export type Time = + | { readonly type: "date"; readonly value: string } + | { readonly type: "number"; readonly value: number } + +export type Operation = + | { readonly op: "chmod"; readonly path: string; readonly mode: number } + | { readonly op: "chown"; readonly path: string; readonly uid: number; readonly gid: number } + | { readonly op: "copy"; readonly from: string; readonly to: string; readonly options?: Options | undefined } + | { readonly op: "copyFile"; readonly from: string; readonly to: string } + | { readonly op: "link"; readonly from: string; readonly to: string } + | { readonly op: "makeDirectory"; readonly path: string; readonly options?: Options | undefined } + | { readonly op: "makeTempDirectory"; readonly options?: Options | undefined } + | { readonly op: "makeTempFile"; readonly options?: Options | undefined } + | { readonly op: "remove"; readonly path: string; readonly options?: Options | undefined } + | { readonly op: "rename"; readonly from: string; readonly to: string } + | { readonly op: "symlink"; readonly from: string; readonly to: string } + | { readonly op: "truncate"; readonly path: string; readonly length?: number | undefined } + | { readonly op: "utimes"; readonly path: string; readonly atime: Time; readonly mtime: Time } + | { + readonly op: "writeFile" + readonly path: string + readonly data: string + readonly options?: Options | undefined + } + | { + readonly op: "writeFileString" + readonly path: string + readonly data: string + readonly options?: Options | undefined + } + +export type BatchOperation = Exclude +export type Request = Operation | { readonly op: "batch"; readonly operations: ReadonlyArray } + +export interface Failure { + readonly name?: string | undefined + readonly message: string + readonly code?: string | undefined + readonly errno?: number | undefined + readonly syscall?: string | undefined + readonly path?: string | undefined + readonly dest?: string | undefined + readonly operation?: Operation["op"] | undefined +} + +export type Response = + | { readonly ok: true; readonly value?: string | undefined } + | { readonly ok: false; readonly error: Failure } + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function isFailure(value: unknown): value is Failure { + return isObject(value) && typeof value.message === "string" +} + +export function isResponse(value: unknown): value is Response { + if (!isObject(value)) return false + if (value.ok === false) return isFailure(value.error) + if (value.ok !== true) return false + return value.value === undefined || typeof value.value === "string" +} + +function isOperation(value: unknown): value is Operation { + if (!isObject(value) || typeof value.op !== "string") return false + const path = typeof value.path === "string" + const from = typeof value.from === "string" + const to = typeof value.to === "string" + switch (value.op) { + case "chmod": + return path && typeof value.mode === "number" + case "chown": + return path && typeof value.uid === "number" && typeof value.gid === "number" + case "copy": + case "copyFile": + case "link": + case "rename": + case "symlink": + return from && to + case "makeDirectory": + case "remove": + case "truncate": + return path + case "makeTempDirectory": + case "makeTempFile": + return true + case "utimes": + return path && isObject(value.atime) && isObject(value.mtime) + case "writeFile": + case "writeFileString": + return path && typeof value.data === "string" + default: + return false + } +} + +function isBatchOperation(value: unknown): value is BatchOperation { + return isOperation(value) && value.op !== "makeTempDirectory" && value.op !== "makeTempFile" +} + +export function isRequest(value: unknown): value is Request { + if (!isObject(value) || typeof value.op !== "string") return false + if (value.op !== "batch") return isOperation(value) + return Array.isArray(value.operations) && value.operations.length > 0 && value.operations.every(isBatchOperation) +} + +export function date(value: Date | number): Time { + return value instanceof Date ? { type: "date", value: value.toISOString() } : { type: "number", value } +} diff --git a/packages/kilo-sandbox/src/mutation.ts b/packages/kilo-sandbox/src/mutation.ts new file mode 100644 index 00000000000..ad5b42862b2 --- /dev/null +++ b/packages/kilo-sandbox/src/mutation.ts @@ -0,0 +1,233 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process" +import { dirname, resolve } from "node:path" +import type { Writable } from "node:stream" +import { finished } from "node:stream/promises" +import { fileURLToPath } from "node:url" +import { Context, Effect, PlatformError } from "effect" +import { confine } from "./backend" +import { isResponse, type BatchOperation, type Failure, type Operation, type Request } from "./mutation-protocol" +import type { Profile } from "./profile" + +declare const KILO_SANDBOX_MUTATION_WORKER_PATH: string + +function worker() { + if (typeof KILO_SANDBOX_MUTATION_WORKER_PATH === "undefined") { + return { path: fileURLToPath(new URL("./kilo-sandbox-mutation-worker.ts", import.meta.url)), environment: {} } + } + const path = KILO_SANDBOX_MUTATION_WORKER_PATH.startsWith(".") + ? fileURLToPath(new URL(KILO_SANDBOX_MUTATION_WORKER_PATH, import.meta.url)) + : resolve(dirname(process.execPath), KILO_SANDBOX_MUTATION_WORKER_PATH) + return { path, environment: { BUN_BE_BUN: "1" } } +} + +function tag(code: string | undefined): PlatformError.SystemErrorTag { + switch (code) { + case "EEXIST": + return "AlreadyExists" + case "EBADF": + case "EISDIR": + case "ELOOP": + case "ENOTDIR": + return "BadResource" + case "EBUSY": + return "Busy" + case "EINVAL": + return "InvalidData" + case "ENOENT": + return "NotFound" + case "EACCES": + case "EPERM": + return "PermissionDenied" + case "ETIMEDOUT": + return "TimedOut" + case "EAGAIN": + return "WouldBlock" + default: + return "Unknown" + } +} + +function failure(method: string, path: string, error: Failure) { + const cause = Object.assign(new Error(error.message), { + name: error.name, + code: error.code, + errno: error.errno, + syscall: error.syscall, + path: error.path, + dest: error.dest, + }) + if (!error.code) { + return PlatformError.badArgument({ module: "FileSystem", method, description: error.message, cause }) + } + return PlatformError.systemError({ + _tag: tag(error.code), + module: "FileSystem", + method, + pathOrDescriptor: error.path ?? path, + syscall: error.syscall, + description: error.message, + cause, + }) +} + +function infrastructure(path: string, description: string, cause?: unknown) { + return PlatformError.systemError({ + _tag: "Unknown", + module: "Sandbox", + method: "mutate", + pathOrDescriptor: path, + description, + cause, + }) +} + +function target(request: Request): string { + if (request.op === "batch") return target(request.operations[0]) + if ("path" in request) return request.path + if ("to" in request) return request.to + return request.options?.directory ?? process.cwd() +} + +function output(stream: NodeJS.ReadableStream) { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + stream.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) + stream.on("end", () => resolve(Buffer.concat(chunks))) + stream.on("error", reject) + }) +} + +function send(stream: Writable, data: string) { + const done = finished(stream) + stream.end(data) + return done +} + +export async function settle( + input: Promise, + stdout: Promise, + stderr: Promise, + exited: Promise, + path: string, +) { + const delivery = input.then( + () => ({ ok: true as const }), + (cause: unknown) => ({ ok: false as const, cause }), + ) + const [sent, out, err, code] = await Promise.all([delivery, stdout, stderr, exited]) + if (code !== 0) { + throw infrastructure( + path, + err.toString("utf8").trim() || `Filesystem worker exited with code ${code}`, + sent.ok ? undefined : sent.cause, + ) + } + if (!sent.ok) throw sent.cause + return out +} + +async function exchange(proc: ChildProcessWithoutNullStreams, data: string, path: string) { + const exited = new Promise((resolve, reject) => { + proc.once("error", reject) + proc.once("close", resolve) + }) + return settle(send(proc.stdin, data), output(proc.stdout), output(proc.stderr), exited, path) +} + +export type Runner = ( + profile: Profile, + request: Request, +) => Effect.Effect + +export const mutate: Runner = (profile, request) => + Effect.scoped( + Effect.gen(function* () { + const child = worker() + const launch = yield* confine(profile, { + command: process.execPath, + args: [child.path], + cwd: process.cwd(), + environment: process.env, + }) + const path = target(request) + const result = yield* Effect.tryPromise({ + try: async (signal) => { + const proc = spawn(launch.command, launch.args, { + cwd: launch.cwd, + env: { ...launch.environment, ...child.environment }, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: process.platform === "win32", + }) + const abort = () => proc.kill() + signal.addEventListener("abort", abort, { once: true }) + const out = await exchange(proc, JSON.stringify(request), path).finally(() => + signal.removeEventListener("abort", abort), + ) + try { + const response: unknown = JSON.parse(out.toString("utf8")) + if (isResponse(response)) return response + throw new TypeError("Invalid filesystem worker response") + } catch (cause) { + throw infrastructure(path, "Filesystem worker returned an invalid response", cause) + } + }, + catch: (cause) => + cause instanceof PlatformError.PlatformError + ? cause + : infrastructure(path, cause instanceof Error ? cause.message : String(cause), cause), + }) + if (!result.ok) return yield* Effect.fail(failure(result.error.operation ?? request.op, path, result.error)) + return result.value + }), + ) + +const CurrentRunner = Context.Reference("@kilocode/sandbox/CurrentMutationRunner", { + defaultValue: () => mutate, +}) + +export const currentRunner: Effect.Effect = Effect.gen(function* () { + return yield* CurrentRunner +}) + +export function withRunner(runner: Runner, effect: Effect.Effect) { + return effect.pipe(Effect.provideService(CurrentRunner, runner)) +} + +function returnsValue( + request: Request, +): request is Extract { + return request.op === "makeTempDirectory" || request.op === "makeTempFile" +} + +export function batchMutations(effect: Effect.Effect) { + return Effect.gen(function* () { + const upstream = yield* currentRunner + const state: { closed: boolean; profile?: Profile; operations: BatchOperation[] } = { + closed: false, + operations: [], + } + const flush = () => + Effect.gen(function* () { + const profile = state.profile + if (!profile || state.operations.length === 0) return + const operations = state.operations.splice(0) + state.profile = undefined + yield* upstream(profile, { op: "batch", operations }) + }) + const collect: Runner = (profile, request) => + Effect.gen(function* () { + if (state.closed) return yield* upstream(profile, request) + if (state.profile && state.profile !== profile) yield* flush() + if (returnsValue(request)) { + yield* flush() + return yield* upstream(profile, request) + } + state.profile = profile + if (request.op === "batch") state.operations.push(...request.operations) + else state.operations.push(request) + return undefined + }) + const close = Effect.sync(() => (state.closed = true)).pipe(Effect.andThen(flush())) + return yield* withRunner(collect, effect).pipe(Effect.onExit(() => close)) + }) +} diff --git a/packages/kilo-sandbox/src/network.ts b/packages/kilo-sandbox/src/network.ts new file mode 100644 index 00000000000..5cd29117f31 --- /dev/null +++ b/packages/kilo-sandbox/src/network.ts @@ -0,0 +1,89 @@ +import { Effect, Layer, PlatformError } from "effect" +import { HttpClient, HttpClientError, type HttpClientRequest } from "effect/unstable/http" +import { current } from "./context" +import type { Profile } from "./profile" + +const proxies = new Set([ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "no_proxy", +]) + +function target(value: string) { + if (!URL.canParse(value)) return value + const url = new URL(value) + return url.origin +} + +function denied(value: string, method: string) { + return PlatformError.systemError({ + _tag: "PermissionDenied", + module: "Sandbox", + method, + pathOrDescriptor: target(value), + description: "Sandbox denied outbound network access", + }) +} + +function unsupported(value: string, method: string) { + return PlatformError.systemError({ + _tag: "BadResource", + module: "Sandbox", + method, + pathOrDescriptor: target(value), + description: "Sandbox proxy network mode and allowedHosts are not supported", + }) +} + +function unsupportedProfile(profile: Profile) { + return profile.network.mode === "proxy" || profile.network.allowedHosts.length > 0 +} + +export function networkEnvironment(profile: Profile, environment: Record) { + if (profile.network.mode === "allow" && profile.network.allowedHosts.length === 0) return environment + return Object.fromEntries(Object.entries(environment).filter(([key]) => !proxies.has(key))) +} + +export function assertProcessNetwork(profile: Profile, command: string) { + if (!unsupportedProfile(profile)) return Effect.void + return Effect.fail(unsupported(command, "prepareNetwork")) +} + +export function assertNetwork(value: string, method = "network") { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return + if (unsupportedProfile(profile)) yield* Effect.fail(unsupported(value, method)) + if (profile.network.mode === "allow") return + yield* Effect.fail(denied(value, method)) + }) +} + +function requestError(request: HttpClientRequest.HttpClientRequest, description: string) { + return new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ request, description }), + }) +} + +function assertRequest(request: HttpClientRequest.HttpClientRequest) { + return Effect.gen(function* () { + const profile = yield* current + if (!profile) return request + if (profile.network.mode === "allow" && profile.network.allowedHosts.length === 0) return request + const description = unsupportedProfile(profile) + ? "Sandbox proxy network mode and allowedHosts are not supported" + : "Sandbox denied outbound network access" + return yield* Effect.fail(requestError(request, description)) + }) +} + +export function decorateHttpClient(http: HttpClient.HttpClient): HttpClient.HttpClient { + return HttpClient.mapRequestEffect(http, assertRequest) +} + +export const httpLayer = Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, decorateHttpClient)) diff --git a/packages/kilo-sandbox/src/path.ts b/packages/kilo-sandbox/src/path.ts new file mode 100644 index 00000000000..1a666b8a2a9 --- /dev/null +++ b/packages/kilo-sandbox/src/path.ts @@ -0,0 +1,92 @@ +import { lstatSync, readlinkSync, realpathSync } from "node:fs" +import path from "node:path" +import { Effect, PlatformError } from "effect" +import type { PathRule, Profile } from "./profile" + +function code(cause: unknown) { + if (typeof cause !== "object" || cause === null || !("code" in cause)) return undefined + return typeof cause.code === "string" ? cause.code : undefined +} + +function resolve(input: string, seen = new Set()) { + const target = path.resolve(input) + if (seen.has(target)) throw Object.assign(new Error("Symlink cycle"), { code: "ELOOP" }) + seen.add(target) + const suffix: Array = [] + let ancestor = target + + while (true) { + try { + return path.resolve(realpathSync.native(ancestor), ...suffix) + } catch (cause) { + const tag = code(cause) + if (tag !== "ENOENT" && tag !== "ENOTDIR") throw cause + try { + if (lstatSync(ancestor).isSymbolicLink()) { + const link = readlinkSync(ancestor) + return resolve(path.resolve(path.dirname(ancestor), link, ...suffix), seen) + } + } catch (error) { + if (code(error) !== "ENOENT" && code(error) !== "ENOTDIR") throw error + } + const parent = path.dirname(ancestor) + if (parent === ancestor) throw cause + suffix.unshift(path.basename(ancestor)) + ancestor = parent + } + } +} + +function attempt(input: string, method: string, fn: () => string): Effect.Effect { + return Effect.try({ + try: fn, + catch: (cause) => + PlatformError.systemError({ + _tag: code(cause) === "EACCES" || code(cause) === "EPERM" ? "PermissionDenied" : "Unknown", + module: "Sandbox", + method, + pathOrDescriptor: input, + description: "Could not resolve the path", + cause, + }), + }) +} + +export function canonicalize(input: string): Effect.Effect { + return attempt(input, "canonicalize", () => resolve(input)) +} + +export function canonicalizeEntry(input: string): Effect.Effect { + return attempt(input, "canonicalizeEntry", () => path.join(resolve(path.dirname(input)), path.basename(input))) +} + +function normalizeRule(rule: PathRule) { + return Effect.map(canonicalize(rule.path), (target): PathRule => ({ path: target, kind: rule.kind })) +} + +export function normalize(profile: Profile): Effect.Effect { + return Effect.gen(function* () { + const allowWrite = yield* Effect.forEach(profile.filesystem.allowWrite, normalizeRule) + const denyWrite = yield* Effect.forEach(profile.filesystem.denyWrite, normalizeRule) + const temporaryDirectory = profile.filesystem.temporaryDirectory + ? yield* canonicalize(profile.filesystem.temporaryDirectory) + : undefined + + return { + ...profile, + filesystem: { + allowWrite, + denyWrite, + denyNames: profile.filesystem.denyNames, + ...(temporaryDirectory === undefined ? {} : { temporaryDirectory }), + }, + } + }) +} + +export function matches(rule: PathRule, target: string) { + const relative = path.relative(rule.path, target) + if (relative === "") return true + if (rule.kind === "literal") return false + return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) +} diff --git a/packages/kilo-sandbox/src/profile.ts b/packages/kilo-sandbox/src/profile.ts new file mode 100644 index 00000000000..2e955779429 --- /dev/null +++ b/packages/kilo-sandbox/src/profile.ts @@ -0,0 +1,29 @@ +export type PathKind = "literal" | "subtree" + +export interface PathRule { + readonly path: string + readonly kind: PathKind +} + +export interface FilesystemProfile { + readonly allowWrite: ReadonlyArray + readonly denyWrite: ReadonlyArray + readonly denyNames: ReadonlyArray + readonly temporaryDirectory?: string | undefined +} + +export interface NetworkProfile { + readonly mode: "allow" | "deny" | "proxy" + readonly allowedHosts: ReadonlyArray +} + +export interface EnvironmentProfile { + readonly deny: ReadonlyArray + readonly set: Readonly> +} + +export interface Profile { + readonly filesystem: FilesystemProfile + readonly network: NetworkProfile + readonly environment: EnvironmentProfile +} diff --git a/packages/kilo-sandbox/src/seatbelt-base.ts b/packages/kilo-sandbox/src/seatbelt-base.ts new file mode 100644 index 00000000000..a439d94611f --- /dev/null +++ b/packages/kilo-sandbox/src/seatbelt-base.ts @@ -0,0 +1,112 @@ +export const base = `(version 1) + +(deny default) + +(allow process-exec) +(allow process-fork) +(allow signal (target same-sandbox)) +(allow process-info* (target same-sandbox)) + +(allow file-write-data + (require-all + (path "/dev/null") + (vnode-type CHARACTER-DEVICE))) + +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.model") + (sysctl-name "hw.memsize") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name-prefix "hw.optional.arm.") + (sysctl-name-prefix "hw.optional.armv8_") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.pagesize") + (sysctl-name "hw.physicalcpu") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.logicalcpu") + (sysctl-name "hw.cpufrequency") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "machdep.cpu.brand_string") + (sysctl-name "kern.argmax") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.maxproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name "vm.loadavg") + (sysctl-name-prefix "hw.perflevel") + (sysctl-name-prefix "kern.proc.pgrp.") + (sysctl-name-prefix "kern.proc.pid.") + (sysctl-name-prefix "net.routetable.") +) + +(allow sysctl-write + (sysctl-name "kern.grade_cputype")) + +(allow iokit-open + (iokit-registry-entry-class "RootDomainUserClient")) + +(allow mach-lookup + (global-name "com.apple.system.opendirectoryd.libinfo")) + +(allow ipc-posix-sem) + +(allow ipc-posix-shm-read-data + ipc-posix-shm-write-create + ipc-posix-shm-write-unlink + (ipc-posix-name-regex #"^/__KMP_REGISTERED_LIB_[0-9]+$")) + +(allow mach-lookup + (global-name "com.apple.PowerManagement.control")) + +(allow pseudo-tty) +(allow file-read* file-write* file-ioctl (literal "/dev/ptmx")) +(allow file-read* file-write* + (require-all + (regex #"^/dev/ttys[0-9]+") + (extension "com.apple.sandbox.pty"))) +(allow file-ioctl (regex #"^/dev/ttys[0-9]+")) + +(allow ipc-posix-shm-read* (ipc-posix-name-prefix "apple.cfprefs.")) +(allow mach-lookup + (global-name "com.apple.cfprefsd.daemon") + (global-name "com.apple.cfprefsd.agent") + (local-name "com.apple.cfprefsd.agent")) +(allow user-preference-read) + +; system services required by common command-line runtimes +(allow system-socket) +(allow mach-lookup + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd")) +(allow sysctl-read (sysctl-name-regex #"^net.routetable")) +` diff --git a/packages/kilo-sandbox/src/seatbelt-network.ts b/packages/kilo-sandbox/src/seatbelt-network.ts new file mode 100644 index 00000000000..3b742d6b88f --- /dev/null +++ b/packages/kilo-sandbox/src/seatbelt-network.ts @@ -0,0 +1,12 @@ +import type { Profile } from "./profile" + +export function networkPolicy(profile: Profile) { + if (profile.network.mode === "allow") { + return "; sandbox network mode: allow\n(allow network-outbound)\n(allow network-inbound)" + } + return [ + `; sandbox network mode: ${profile.network.mode}`, + '(deny network-outbound (with message "Sandbox denied outbound network access"))', + "(allow network-inbound)", + ].join("\n") +} diff --git a/packages/kilo-sandbox/src/seatbelt.ts b/packages/kilo-sandbox/src/seatbelt.ts new file mode 100644 index 00000000000..ad2bb9ac9df --- /dev/null +++ b/packages/kilo-sandbox/src/seatbelt.ts @@ -0,0 +1,81 @@ +import { existsSync } from "node:fs" +import { Effect } from "effect" +import type { Backend, Launch, Support } from "./backend" +import type { PathRule, Profile } from "./profile" +import { base } from "./seatbelt-base" +import { networkPolicy } from "./seatbelt-network" + +const executable = "/usr/bin/sandbox-exec" + +interface Param { + readonly key: string + readonly value: string +} + +function filter(rule: PathRule, key: string) { + if (rule.kind === "literal") return `(literal (param "${key}"))` + return `(require-any (literal (param "${key}")) (subpath (param "${key}")))` +} + +function escape(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +function quote(value: string) { + return `'${value.replaceAll("'", `'\\''`)}'` +} + +function exclude(rule: PathRule, key: string) { + if (rule.kind === "literal") return [`(require-not (literal (param "${key}")))`] + return [`(require-not (literal (param "${key}")))`, `(require-not (subpath (param "${key}")))`] +} + +function policy(profile: Profile) { + const params: Array = [] + const allow = profile.filesystem.allowWrite.map((rule, index) => { + const key = `ALLOW_WRITE_${index}` + params.push({ key, value: rule.path }) + return filter(rule, key) + }) + const deny = profile.filesystem.denyWrite.flatMap((rule, index) => { + const key = `DENY_WRITE_${index}` + params.push({ key, value: rule.path }) + return exclude(rule, key) + }) + const names = profile.filesystem.denyNames.map((name) => `(require-not (regex #"(^|/)${escape(name)}(/|$)"))`) + const write = + allow.length === 0 + ? "" + : `(allow file-write*\n (require-all\n (require-any ${allow.join(" ")})\n ${[...deny, ...names].join("\n ")}\n )\n)` + return { + value: [ + base, + networkPolicy(profile), + "; reads are not confined by the file-level sandbox\n(allow file-read*)", + write, + ].join("\n"), + params, + } +} + +export function generate(profile: Profile, launch: Launch): Launch { + const generated = policy(profile) + const args = ["-p", generated.value, ...generated.params.map((param) => `-D${param.key}=${param.value}`)] + const command = launch.shell ? (typeof launch.shell === "string" ? launch.shell : "/bin/sh") : launch.command + const commandArgs = launch.shell ? ["-c", [launch.command, ...launch.args.map(quote)].join(" ")] : launch.args + args.push("--", command, ...commandArgs) + return { + ...launch, + command: executable, + args, + } +} + +const available: Support = existsSync(executable) + ? { available: true } + : { available: false, reason: `${executable} is not available` } + +export const seatbelt: Backend = { + support: () => available, + prepare: (profile, launch) => Effect.succeed(generate(profile, launch)), +} diff --git a/packages/kilo-sandbox/test/backend.test.ts b/packages/kilo-sandbox/test/backend.test.ts new file mode 100644 index 00000000000..0555032b586 --- /dev/null +++ b/packages/kilo-sandbox/test/backend.test.ts @@ -0,0 +1,265 @@ +import { describe, expect, test } from "bun:test" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import os from "node:os" +import path from "node:path" +import { Effect, PlatformError, Result } from "effect" +import { backendSupport, confine, prepare, type Launch } from "../src/backend" +import { generate as generateBubblewrap, parseMountinfo } from "../src/bubblewrap" +import { run } from "../src/context" +import { settle } from "../src/mutation" +import type { Profile } from "../src/profile" +import { generate } from "../src/seatbelt" + +function makeProfile(mode: Profile["network"]["mode"] = "deny"): Profile { + return { + filesystem: { + allowWrite: [{ path: "/workspace", kind: "subtree" }], + denyWrite: [{ path: "/workspace/.git", kind: "subtree" }], + denyNames: [".git"], + }, + network: { mode, allowedHosts: mode === "proxy" ? ["example.com"] : [] }, + environment: { deny: ["DROP", "RESET"], set: { KEEP: "profile", RESET: "removed" } }, + } +} + +const launch: Launch = { + command: "/bin/echo", + args: ["hello"], + cwd: "/workspace", + environment: { + KEEP: "launch", + DROP: "secret", + HTTPS_PROXY: "http://127.0.0.1:9000", + no_proxy: "*", + }, +} + +describe("sandbox launch preparation", () => { + test("generates a globally overriding overlapping deny policy with parameterized paths", () => { + const result = generate(makeProfile(), launch) + const policy = result.args[1] + expect(policy).toContain('(require-any (literal (param "ALLOW_WRITE_0")) (subpath (param "ALLOW_WRITE_0")))') + expect(policy).toContain('(require-not (literal (param "DENY_WRITE_0")))') + expect(policy).toContain('(require-not (subpath (param "DENY_WRITE_0")))') + expect(policy).toContain('(require-not (regex #"(^|/)\\.git(/|$)"))') + expect(policy).toContain("(allow file-read*)") + expect(policy).toContain("sandbox network mode: deny") + expect(policy).toContain("(deny network-outbound") + expect(policy).not.toContain("(allow network-outbound)") + expect(policy).toContain("(allow network-inbound)") + expect(policy).not.toContain("/workspace/.git") + expect(result.args).toContain("-DALLOW_WRITE_0=/workspace") + expect(result.args).toContain("-DDENY_WRITE_0=/workspace/.git") + expect(result.args.slice(-3)).toEqual(["--", "/bin/echo", "hello"]) + }) + + test("preserves unrestricted networking in allow mode", () => { + const result = generate(makeProfile("allow"), launch) + const policy = result.args[1] + expect(policy).toContain("sandbox network mode: allow") + expect(policy).toContain("(allow network-outbound)") + expect(policy).toContain("(allow network-inbound)") + expect(policy).not.toContain("(deny network-outbound") + }) + + test("places shell commands inside the sandbox backend", () => { + const result = generate(makeProfile(), { ...launch, command: "echo hello", args: [], shell: "/bin/zsh" }) + expect(result.args.slice(-4)).toEqual(["--", "/bin/zsh", "-c", "echo hello"]) + + const args = generate(makeProfile(), { + ...launch, + command: "printf", + args: ["%s", "hello world"], + shell: true, + }) + expect(args.args.slice(-4)).toEqual(["--", "/bin/sh", "-c", "printf '%s' 'hello world'"]) + }) + + test("keeps the host network namespace in Linux allow mode", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-policy-")) + const git = path.join(root, ".git") + mkdirSync(git) + writeFileSync(path.join(git, "config"), "original") + const profile: Profile = { + ...makeProfile("allow"), + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [".git"], + }, + } + + try { + const result = generateBubblewrap(profile, { ...launch, cwd: root }, "/opt/kilo/bwrap") + const writable = result.args.indexOf("--bind") + const protectedPath = result.args.indexOf("--ro-bind", writable + 1) + expect(result.command).toBe("/opt/kilo/bwrap") + expect(writable).toBeGreaterThan(-1) + expect(protectedPath).toBeGreaterThan(writable) + expect(result.args.slice(protectedPath, protectedPath + 3)).toEqual(["--ro-bind", git, git]) + expect(result.args).not.toContain("--unshare-net") + expect(result.args.slice(-3)).toEqual(["--", "/bin/echo", "hello"]) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + + test("isolates the Linux network namespace in deny mode", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-network-")) + const input = makeProfile("deny") + const profile: Profile = { + ...input, + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [], + }, + } + + try { + const result = generateBubblewrap(profile, { ...launch, cwd: root }, "/opt/kilo/bwrap") + expect(result.args).toContain("--unshare-net") + expect(result.args.indexOf("--unshare-net")).toBeGreaterThan(result.args.indexOf("--unshare-pid")) + expect(result.args.slice(-3)).toEqual(["--", "/bin/echo", "hello"]) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + + test("parses escaped mount points from Linux mountinfo", () => { + const content = [ + String.raw`36 25 0:32 / / rw,relatime - overlay overlay rw`, + String.raw`37 36 0:33 / /tmp/kilo\040root rw - tmpfs tmpfs rw`, + String.raw`38 37 0:34 / /tmp/kilo\040root/nested\011mount rw - tmpfs tmpfs rw`, + String.raw`39 36 0:35 / /tmp/back\134slash rw - tmpfs tmpfs rw`, + "", + ].join("\n") + + expect(parseMountinfo(content)).toEqual(["/", "/tmp/kilo root", "/tmp/kilo root/nested\tmount", "/tmp/back\\slash"]) + }) + + test("allows a mounted writable root but rejects nested mount points", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-mount-")) + const nested = path.join(root, "nested mount") + const profile: Profile = { + ...makeProfile("allow"), + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [], + }, + } + + try { + expect(() => generateBubblewrap(profile, launch, "/opt/kilo/bwrap", [root])).not.toThrow() + expect(() => generateBubblewrap(profile, launch, "/opt/kilo/bwrap", [root, nested])).toThrow( + `Writable root contains a nested mount point: ${nested}`, + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + + test("rejects a Bubblewrap executable inside a writable root", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-helper-")) + const helper = path.join(root, "bwrap") + writeFileSync(helper, "helper") + const profile: Profile = { + ...makeProfile("allow"), + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [], + }, + } + + try { + expect(() => generateBubblewrap(profile, launch, helper)).toThrow("writable by the sandbox profile") + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + + test("passes the launch through unchanged when no profile is active", async () => { + const result = await Effect.runPromise(Effect.scoped(prepare(launch))) + expect(result.command).toBe(launch.command) + expect(result.args).toBe(launch.args) + expect(result.cwd).toBe(launch.cwd) + expect(result.environment).toBe(launch.environment) + }) + + test("merges profile environment values and applies exact deny names", async () => { + const result = await Effect.runPromise(Effect.scoped(run(makeProfile("allow"), prepare(launch)))) + expect(result.environment?.KEEP).toBe("profile") + expect(result.environment?.DROP).toBeUndefined() + expect(result.environment?.RESET).toBeUndefined() + expect(result.environment?.HTTPS_PROXY).toBe("http://127.0.0.1:9000") + expect(result.environment?.no_proxy).toBe("*") + expect(result.environment?.PATH).toBeUndefined() + }) + + test("fails proxy mode closed before launching a process", async () => { + const result = await Effect.runPromise( + Effect.scoped(run(makeProfile("proxy"), prepare(launch))).pipe(Effect.result), + ) + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure.reason._tag).toBe("BadResource") + expect(result.failure.message).toContain("proxy network mode and allowedHosts are not supported") + } + }) + + test("fails non-empty allowedHosts closed before launching a process", async () => { + const input = makeProfile("allow") + const result = await Effect.runPromise( + Effect.scoped(run({ ...input, network: { mode: "allow", allowedHosts: ["example.com"] } }, prepare(launch))).pipe( + Effect.result, + ), + ) + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure.reason._tag).toBe("BadResource") + expect(result.failure.message).toContain("proxy network mode and allowedHosts are not supported") + } + }) + + test("fails allowed-host profiles closed through explicit confinement", async () => { + const input = makeProfile("allow") + const result = await Effect.runPromise( + Effect.scoped(confine({ ...input, network: { mode: "allow", allowedHosts: ["example.com"] } }, launch)).pipe( + Effect.result, + ), + ) + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure.reason._tag).toBe("BadResource") + expect(result.failure.message).toContain("proxy network mode and allowedHosts are not supported") + } + }) + + test("preserves worker stderr when the request pipe also fails", async () => { + const pipe = Object.assign(new Error("write EPIPE"), { code: "EPIPE" }) + const cause = await settle( + Promise.reject(pipe), + Promise.resolve(Buffer.alloc(0)), + Promise.resolve(Buffer.from("useful worker failure")), + Promise.resolve(7), + "/workspace/value.txt", + ).then( + () => undefined, + (error: unknown) => error, + ) + expect(cause).toBeInstanceOf(PlatformError.PlatformError) + if (!(cause instanceof PlatformError.PlatformError)) return + expect(cause.reason.description).toBe("useful worker failure") + expect(cause.reason.cause).toBe(pipe) + }) + + test("reports backend support with a reason when unavailable", () => { + for (const network of [undefined, makeProfile("deny").network]) { + const support = backendSupport(network) + expect(typeof support.available).toBe("boolean") + if (!support.available) expect(support.reason?.length).toBeGreaterThan(0) + } + }) +}) diff --git a/packages/kilo-sandbox/test/context.test.ts b/packages/kilo-sandbox/test/context.test.ts new file mode 100644 index 00000000000..5eb79d7da08 --- /dev/null +++ b/packages/kilo-sandbox/test/context.test.ts @@ -0,0 +1,92 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test" +import { mkdir, mkdtemp, realpath, rm, symlink } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { Effect } from "effect" +import { assertWrite, current, enabled, run } from "../src/context" +import type { Profile } from "../src/profile" + +function makeProfile( + allowWrite: Profile["filesystem"]["allowWrite"], + denyWrite: Profile["filesystem"]["denyWrite"] = [], + denyNames: Profile["filesystem"]["denyNames"] = [], +): Profile { + return { + filesystem: { allowWrite, denyWrite, denyNames }, + network: { mode: "allow", allowedHosts: [] }, + environment: { deny: [], set: {} }, + } +} + +describe("sandbox profile context", () => { + let root = "" + + beforeAll(async () => { + root = await realpath(await mkdtemp(path.join(tmpdir(), "kilo-sandbox-context-"))) + }) + + afterAll(async () => { + await rm(root, { recursive: true, force: true }) + }) + + test("is disabled outside run and exposes the normalized current profile inside run", async () => { + expect(await Effect.runPromise(enabled)).toBe(false) + const value = await Effect.runPromise( + run(makeProfile([{ path: root, kind: "subtree" }]), Effect.all([enabled, current])), + ) + expect(value[0]).toBe(true) + expect(value[1]?.filesystem.allowWrite[0]?.path).toBe(root) + expect(await Effect.runPromise(current)).toBeUndefined() + }) + + test("rejects writes outside the allowed roots", async () => { + const error = await Effect.runPromise( + run(makeProfile([]), assertWrite(path.join(root, "outside.txt")).pipe(Effect.flip)), + ) + expect(error.reason._tag).toBe("PermissionDenied") + }) + + test("applies deny rules before overlapping allows", async () => { + const denied = path.join(root, ".git") + const target = path.join(denied, "config") + const profile = makeProfile([{ path: root, kind: "subtree" }], [{ path: denied, kind: "subtree" }]) + const error = await Effect.runPromise(run(profile, assertWrite(target).pipe(Effect.flip))) + expect(error.reason._tag).toBe("PermissionDenied") + }) + + test("applies denied path names under allowed roots", async () => { + const target = path.join(root, "external", ".git", "config") + const error = await Effect.runPromise( + run(makeProfile([{ path: root, kind: "subtree" }], [], [".git"]), assertWrite(target).pipe(Effect.flip)), + ) + expect(error.reason._tag).toBe("PermissionDenied") + }) + + test("canonicalizes the longest existing ancestor across symlinks", async () => { + const allowed = path.join(root, "allowed") + const outside = path.join(root, "outside") + await mkdir(allowed) + await mkdir(outside) + await symlink(outside, path.join(allowed, "link"), "junction") + + const error = await Effect.runPromise( + run( + makeProfile([{ path: allowed, kind: "subtree" }]), + assertWrite(path.join(allowed, "link", "new.txt")).pipe(Effect.flip), + ), + ) + expect(error.reason._tag).toBe("PermissionDenied") + }) + + test("resolves dangling symlinks before authorizing a write", async () => { + const allowed = path.join(root, "dangling-allowed") + const outside = path.join(root, "dangling-outside.txt") + await mkdir(allowed) + await symlink(outside, path.join(allowed, "link")) + + const error = await Effect.runPromise( + run(makeProfile([{ path: allowed, kind: "subtree" }]), assertWrite(path.join(allowed, "link")).pipe(Effect.flip)), + ) + expect(error.reason._tag).toBe("PermissionDenied") + }) +}) diff --git a/packages/kilo-sandbox/test/filesystem.test.ts b/packages/kilo-sandbox/test/filesystem.test.ts new file mode 100644 index 00000000000..1d22fe8b3c4 --- /dev/null +++ b/packages/kilo-sandbox/test/filesystem.test.ts @@ -0,0 +1,349 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test" +import { lstat, mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { NodeFileSystem } from "@effect/platform-node" +import { Effect, FileSystem, Layer, Scope, Stream } from "effect" +import { run } from "../src/context" +import { layer } from "../src/filesystem" +import { batchMutations, currentRunner, withRunner, type Runner } from "../src/mutation" +import type { Request } from "../src/mutation-protocol" +import type { Profile } from "../src/profile" + +const live = layer.pipe(Layer.provide(NodeFileSystem.layer)) + +function makeProfile(root: string, temporaryDirectory?: string): Profile { + return { + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [], + ...(temporaryDirectory === undefined ? {} : { temporaryDirectory }), + }, + network: { mode: "allow", allowedHosts: [] }, + environment: { deny: [], set: {} }, + } +} + +function execute(effect: Effect.Effect) { + return Effect.runPromise(effect.pipe(Effect.provide(live), Effect.scoped)) +} + +describe("sandbox FileSystem", () => { + let root = "" + let allowed = "" + let outside = "" + + beforeAll(async () => { + root = await realpath(await mkdtemp(path.join(tmpdir(), "kilo-sandbox-filesystem-"))) + allowed = path.join(root, "allowed") + outside = path.join(root, "outside.txt") + await writeFile(outside, "outside") + }) + + afterAll(async () => { + await rm(root, { recursive: true, force: true }) + }) + + test.skipIf(process.platform !== "darwin")( + "guards writes with PermissionDenied and forwards mutation options through Seatbelt", + async () => { + await execute( + run( + makeProfile(allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const nested = path.join(allowed, "nested", "directory") + yield* fs.makeDirectory(nested, { recursive: true, mode: 0o700 }) + const file = path.join(nested, "value.txt") + yield* fs.writeFileString(file, "first", { flag: "wx", mode: 0o600 }) + const exists = yield* fs.writeFileString(file, "second", { flag: "wx" }).pipe(Effect.flip) + expect(exists.reason._tag).toBe("AlreadyExists") + const denied = yield* fs.writeFileString(outside, "blocked").pipe(Effect.flip) + expect(denied.reason._tag).toBe("PermissionDenied") + }), + ), + ) + expect(await readFile(path.join(allowed, "nested", "directory", "value.txt"), "utf8")).toBe("first") + expect(await readFile(outside, "utf8")).toBe("outside") + }, + ) + + test("batches nested finite mutations in request order", async () => { + await mkdir(allowed, { recursive: true }) + const requests: Request[] = [] + const runner: Runner = (_profile, request) => Effect.sync(() => requests.push(request)).pipe(Effect.as(undefined)) + await execute( + withRunner( + runner, + run( + makeProfile(allowed), + batchMutations( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.makeDirectory(path.join(allowed, "nested"), { recursive: true }) + yield* batchMutations(fs.writeFileString(path.join(allowed, "nested", "value.txt"), "value")) + yield* fs.chmod(path.join(allowed, "nested", "value.txt"), 0o600) + }), + ), + ), + ), + ) + expect(requests).toHaveLength(1) + expect(requests[0]).toMatchObject({ + op: "batch", + operations: [{ op: "makeDirectory" }, { op: "writeFileString" }, { op: "chmod" }], + }) + }) + + test("flushes queued mutations before propagating a later failure", async () => { + await mkdir(allowed, { recursive: true }) + const requests: Request[] = [] + const runner: Runner = (_profile, request) => Effect.sync(() => requests.push(request)).pipe(Effect.as(undefined)) + const exit = await execute( + withRunner( + runner, + run( + makeProfile(allowed), + batchMutations( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.writeFileString(path.join(allowed, "value.txt"), "value") + return yield* Effect.fail("later failure") + }), + ), + ), + ).pipe(Effect.exit), + ) + expect(exit._tag).toBe("Failure") + expect(requests).toHaveLength(1) + expect(requests[0]).toMatchObject({ op: "batch", operations: [{ op: "writeFileString" }] }) + }) + + test("delegates runners retained after a batch closes", async () => { + const requests: Request[] = [] + const runner: Runner = (_profile, request) => Effect.sync(() => requests.push(request)).pipe(Effect.as(undefined)) + const escaped = await execute(withRunner(runner, batchMutations(currentRunner))) + await execute(escaped(makeProfile(allowed), { op: "remove", path: path.join(allowed, "value.txt") })) + expect(requests).toEqual([{ op: "remove", path: path.join(allowed, "value.txt") }]) + }) + + test("delegates retained runners while the final batch flush is in flight", async () => { + const requests: Request[] = [] + const started = Promise.withResolvers() + const release = Promise.withResolvers() + const profile = makeProfile(allowed) + const queued: Request = { op: "remove", path: path.join(allowed, "queued.txt") } + const late: Request = { op: "remove", path: path.join(allowed, "late.txt") } + const runner: Runner = (_profile, request) => + Effect.promise(() => { + requests.push(request) + if (requests.length === 1) { + started.resolve() + return release.promise.then(() => undefined) + } + return Promise.resolve(undefined) + }) + let escaped: Runner | undefined + const closing = execute( + withRunner( + runner, + batchMutations( + Effect.gen(function* () { + escaped = yield* currentRunner + yield* escaped(profile, queued) + }), + ), + ), + ) + + await started.promise + if (!escaped) throw new Error("Mutation runner did not escape") + try { + await execute(escaped(profile, late)) + expect(requests).toEqual([{ op: "batch", operations: [queued] }, late]) + } finally { + release.resolve() + await closing + } + }) + + test("allows read-only open but guards writable open and sink", async () => { + const inside = path.join(allowed, "open.txt") + await mkdir(allowed, { recursive: true }) + await writeFile(inside, "inside") + await execute( + run( + makeProfile(allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.open(outside, { flag: "r" }) + const open = yield* fs.open(outside, { flag: "r+" }).pipe(Effect.flip) + expect(open.reason._tag).toBe("PermissionDenied") + const restricted = yield* fs.open(inside, { flag: "r+" }).pipe(Effect.flip) + expect(restricted.reason._tag).toBe("PermissionDenied") + expect(restricted.reason.description).toContain("Writable file handles") + const sink = yield* Stream.run(Stream.make(new TextEncoder().encode("blocked")), fs.sink(outside)).pipe( + Effect.flip, + ) + expect(sink.reason._tag).toBe("PermissionDenied") + }), + ), + ) + }) + + test.skipIf(process.platform !== "darwin")( + "redirects default temporary files and directories and preserves their options", + async () => { + await execute( + run( + makeProfile(allowed, allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.makeDirectory(allowed, { recursive: true }) + const directory = yield* fs.makeTempDirectory({ prefix: "directory-" }) + const file = yield* fs.makeTempFile({ prefix: "file-", suffix: ".txt" }) + expect(path.dirname(directory)).toBe(allowed) + expect(path.basename(directory).startsWith("directory-")).toBe(true) + expect(path.dirname(path.dirname(file))).toBe(allowed) + expect(path.basename(path.dirname(file)).startsWith("file-")).toBe(true) + expect(file.endsWith(".txt")).toBe(true) + }), + ), + ) + }, + ) + + test.skipIf(process.platform !== "darwin")( + "preserves finite mutation data, options, timestamps, and links", + async () => { + const base = path.join(allowed, "operations") + const source = path.join(base, "source.bin") + const copy = path.join(base, "copy.bin") + const tree = path.join(base, "tree") + const copied = path.join(base, "copied") + const hard = path.join(base, "hard.bin") + const symbolic = path.join(base, "symbolic.bin") + const streamed = path.join(base, "streamed.bin") + const time = new Date("2024-01-02T03:04:05.000Z") + const copiedTime = new Date("2023-02-03T04:05:06.000Z") + await execute( + run( + makeProfile(allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.makeDirectory(tree, { recursive: true, mode: 0o700 }) + yield* fs.writeFile(source, Uint8Array.from([0, 255, 1, 254]), { flag: "wx", mode: 0o600 }) + yield* fs.chmod(source, 0o640) + yield* fs.chown(source, process.getuid!(), process.getgid!()) + yield* fs.truncate(source, 3) + yield* fs.utimes(source, time, time) + yield* fs.copyFile(source, copy) + const value = path.join(tree, "value.txt") + yield* fs.writeFileString(value, "tree") + yield* fs.utimes(value, copiedTime, copiedTime) + yield* fs.copy(tree, copied, { overwrite: true, preserveTimestamps: true }) + yield* fs.link(source, hard) + yield* fs.symlink("source.bin", symbolic) + yield* Stream.run( + Stream.make(Uint8Array.from([1, 2]), Uint8Array.from([3, 4])), + fs.sink(streamed, { flag: "wx", mode: 0o600 }), + ) + }), + ), + ) + expect([...(await readFile(source))]).toEqual([0, 255, 1]) + expect([...(await readFile(copy))]).toEqual([0, 255, 1]) + expect([...(await readFile(hard))]).toEqual([0, 255, 1]) + expect([...(await readFile(symbolic))]).toEqual([0, 255, 1]) + expect([...(await readFile(streamed))]).toEqual([1, 2, 3, 4]) + expect(await readFile(path.join(copied, "value.txt"), "utf8")).toBe("tree") + const info = await stat(source) + const copiedInfo = await stat(path.join(copied, "value.txt")) + expect(info.mode & 0o777).toBe(0o640) + expect(info.mtime.getTime()).toBe(time.getTime()) + expect(copiedInfo.mtime.getTime()).toBe(copiedTime.getTime()) + }, + ) + + test.skipIf(process.platform !== "darwin")( + "cleans up scoped temporary files and directories through Seatbelt", + async () => { + const created: string[] = [] + await execute( + run( + makeProfile(allowed, allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + created.push(yield* fs.makeTempDirectoryScoped({ prefix: "scoped-directory-" })) + created.push(yield* fs.makeTempFileScoped({ prefix: "scoped-file-", suffix: ".txt" })) + }), + ), + ) + for (const item of created) { + expect( + await lstat(item).then( + () => true, + () => false, + ), + ).toBe(false) + } + }, + ) + + test.skipIf(process.platform !== "darwin")( + "removes and renames allowed symlink entries without following their targets", + async () => { + await mkdir(allowed, { recursive: true }) + const removed = path.join(allowed, "removed-link") + const renamed = path.join(allowed, "renamed-link") + const moved = path.join(allowed, "moved-link") + await symlink(outside, removed) + await symlink(outside, renamed) + + await execute( + run( + makeProfile(allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.remove(removed) + yield* fs.rename(renamed, moved) + }), + ), + ) + const missing = await lstat(removed).then( + () => false, + () => true, + ) + expect(missing).toBe(true) + expect((await lstat(moved)).isSymbolicLink()).toBe(true) + expect(await readFile(outside, "utf8")).toBe("outside") + }, + ) + + test.skipIf(process.platform === "darwin" || process.platform === "linux")( + "fails closed when the OS backend is unavailable", + async () => { + await execute( + run( + makeProfile(allowed), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const denied = yield* fs.writeFileString(path.join(allowed, "blocked.txt"), "blocked").pipe(Effect.flip) + expect(denied.reason._tag).toBe("PermissionDenied") + }), + ), + ) + }, + ) + + test("passes through mutations when no profile is active", async () => { + await execute( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + yield* fs.writeFileString(outside, "passthrough") + }), + ) + expect(await readFile(outside, "utf8")).toBe("passthrough") + }) +}) diff --git a/packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts b/packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts new file mode 100644 index 00000000000..bb61520907d --- /dev/null +++ b/packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts @@ -0,0 +1,85 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdtemp, readFile, rm, stat } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { isResponse, type Request } from "../src/mutation-protocol" + +const roots: string[] = [] + +async function worker(request: Request) { + const entry = fileURLToPath(new URL("../src/kilo-sandbox-mutation-worker.ts", import.meta.url)) + const proc = Bun.spawn([process.execPath, entry], { + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }) + await proc.stdin.write(JSON.stringify(request)) + await proc.stdin.end() + const [stdout, stderr, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]) + if (code !== 0) throw new Error(stderr || `Filesystem worker exited ${code}`) + const response: unknown = JSON.parse(stdout) + if (!isResponse(response)) throw new Error("Filesystem worker returned an invalid response") + return response +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe("filesystem mutation worker", () => { + test("executes ordered mutation batches", async () => { + const root = await mkdtemp(path.join(tmpdir(), "kilo-mutation-worker-")) + roots.push(root) + const dir = path.join(root, "nested") + const file = path.join(dir, "value.txt") + const response = await worker({ + op: "batch", + operations: [ + { op: "makeDirectory", path: dir, options: { recursive: true } }, + { op: "writeFileString", path: file, data: "batched" }, + { op: "chmod", path: file, mode: 0o640 }, + ], + }) + + expect(response).toEqual({ ok: true }) + expect(await readFile(file, "utf8")).toBe("batched") + if (process.platform !== "win32") expect((await stat(file)).mode & 0o777).toBe(0o640) + }) + + test("serializes single-operation filesystem failures", async () => { + const root = await mkdtemp(path.join(tmpdir(), "kilo-mutation-worker-")) + roots.push(root) + const response = await worker({ op: "writeFileString", path: path.join(root, "missing", "value.txt"), data: "x" }) + + expect(response.ok).toBe(false) + if (response.ok) return + expect(response.error.message).toContain("no such file or directory") + expect(response.error.operation).toBe("writeFileString") + expect(response.error.code).toBe("ENOENT") + }) + + test("reports the failed operation and stops the remaining batch", async () => { + const root = await mkdtemp(path.join(tmpdir(), "kilo-mutation-worker-")) + roots.push(root) + const missing = path.join(root, "missing", "value.txt") + const skipped = path.join(root, "skipped.txt") + const response = await worker({ + op: "batch", + operations: [ + { op: "writeFileString", path: missing, data: "blocked" }, + { op: "writeFileString", path: skipped, data: "skipped" }, + ], + }) + + expect(response.ok).toBe(false) + if (response.ok) return + expect(response.error.operation).toBe("writeFileString") + expect(response.error.code).toBe("ENOENT") + expect(await Bun.file(skipped).exists()).toBe(false) + }) +}) diff --git a/packages/kilo-sandbox/test/network.test.ts b/packages/kilo-sandbox/test/network.test.ts new file mode 100644 index 00000000000..380fdc758f4 --- /dev/null +++ b/packages/kilo-sandbox/test/network.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Result } from "effect" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { run } from "../src/context" +import { assertNetwork, decorateHttpClient } from "../src/network" +import type { Profile } from "../src/profile" + +function profile(mode: Profile["network"]["mode"]): Profile { + return { + filesystem: { + allowWrite: [{ path: process.cwd(), kind: "subtree" }], + denyWrite: [], + denyNames: [".git"], + }, + network: { mode, allowedHosts: mode === "proxy" ? ["example.com"] : [] }, + environment: { deny: [], set: {} }, + } +} + +function server() { + const paths: string[] = [] + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(request) { + const path = new URL(request.url).pathname + paths.push(path) + return new Response(path) + }, + }) + return { server, paths } +} + +describe("sandbox in-process network capability", () => { + test("keeps concurrent allow, deny, and control-plane requests call-local", async () => { + const http = server() + try { + const effect = Effect.gen(function* () { + const raw = yield* HttpClient.HttpClient + const guarded = decorateHttpClient(raw) + return yield* Effect.all( + { + denied: run(profile("deny"), guarded.get(new URL("/denied", http.server.url))).pipe(Effect.result), + allowed: run( + profile("allow"), + Effect.flatMap(guarded.get(new URL("/allowed", http.server.url)), (response) => response.text), + ), + control: run( + profile("deny"), + Effect.flatMap(raw.get(new URL("/control", http.server.url)), (response) => response.text), + ), + }, + { concurrency: "unbounded" }, + ) + }).pipe(Effect.provide(FetchHttpClient.layer)) + + const result = await Effect.runPromise(effect) + expect(Result.isFailure(result.denied)).toBe(true) + if (Result.isFailure(result.denied)) { + expect(result.denied.failure.message).toContain("Sandbox denied outbound network access") + } + expect(result.allowed).toBe("/allowed") + expect(result.control).toBe("/control") + expect(http.paths.sort()).toEqual(["/allowed", "/control"]) + } finally { + await http.server.stop(true) + } + }) + + test("fails closed when allowedHosts is set outside proxy mode", async () => { + for (const mode of ["allow", "deny"] as const) { + const input = profile(mode) + const result = await Effect.runPromise( + run( + { ...input, network: { mode, allowedHosts: ["example.com"] } }, + assertNetwork("https://example.com", "testRequest"), + ).pipe(Effect.result), + ) + expect(Result.isFailure(result)).toBe(true) + if (Result.isFailure(result)) { + expect(result.failure.message).toContain("proxy network mode and allowedHosts are not supported") + } + } + }) + + test("fails closed with a clear unsupported result for proxy mode", async () => { + const http = server() + try { + const result = await Effect.runPromise( + Effect.gen(function* () { + const raw = yield* HttpClient.HttpClient + const guarded = decorateHttpClient(raw) + return yield* Effect.all({ + capability: run(profile("proxy"), assertNetwork("https://example.com/path", "testRequest")).pipe( + Effect.result, + ), + request: run(profile("proxy"), guarded.get(new URL("/proxy", http.server.url))).pipe(Effect.result), + }) + }).pipe(Effect.provide(FetchHttpClient.layer)), + ) + expect(Result.isFailure(result.capability)).toBe(true) + if (Result.isFailure(result.capability)) { + expect(result.capability.failure.reason._tag).toBe("BadResource") + expect(result.capability.failure.message).toContain("proxy network mode and allowedHosts are not supported") + expect(result.capability.failure.message).toContain("https://example.com") + expect(result.capability.failure.message).not.toContain("/path") + } + expect(Result.isFailure(result.request)).toBe(true) + if (Result.isFailure(result.request)) { + expect(result.request.failure.message).toContain("proxy network mode and allowedHosts are not supported") + } + expect(http.paths).toEqual([]) + } finally { + await http.server.stop(true) + } + }) +}) diff --git a/packages/kilo-sandbox/test/seatbelt-network.test.ts b/packages/kilo-sandbox/test/seatbelt-network.test.ts new file mode 100644 index 00000000000..bb2eb4dead6 --- /dev/null +++ b/packages/kilo-sandbox/test/seatbelt-network.test.ts @@ -0,0 +1,257 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { prepare, type Launch } from "../src/backend" +import { run } from "../src/context" +import type { Profile } from "../src/profile" + +const mac = process.platform === "darwin" ? test : test.skip +const roots: string[] = [] + +function profile(root: string, mode: Profile["network"]["mode"]): Profile { + return { + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [".protected"], + }, + network: { mode, allowedHosts: [] }, + environment: { deny: [], set: {} }, + } +} + +function prepareLaunch(profile: Profile, input: Launch) { + return Effect.runPromise(Effect.scoped(run(profile, prepare(input)))) +} + +async function launch(profile: Profile, input: Launch) { + const target = await prepareLaunch(profile, input) + const child = Bun.spawn([target.command, ...target.args], { + cwd: target.cwd, + env: target.environment, + stdout: "pipe", + stderr: "pipe", + }) + const [code, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + return { code, stdout, stderr } +} + +function server(hostname: string) { + let accepted = 0 + const listener = Bun.listen({ + hostname, + port: 0, + socket: { + open(socket) { + accepted++ + socket.write("sandbox-tcp-ok") + socket.end() + }, + data() {}, + }, + }) + return { + listener, + accepted: () => accepted, + } +} + +function http() { + let requests = 0 + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { + requests++ + return new Response("sandbox-http-ok") + }, + }) + return { server, requests: () => requests } +} + +async function root() { + const dir = await mkdtemp(join(tmpdir(), "kilo-seatbelt-network-")) + roots.push(dir) + await mkdir(join(dir, ".protected")) + return dir +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +describe("macOS Seatbelt network integration", () => { + mac("allows a sandboxed child to exchange loopback TCP data in allow mode", async () => { + const dir = await root() + const tcp = server("127.0.0.1") + try { + const result = await launch(profile(dir, "allow"), { + command: "/usr/bin/nc", + args: ["127.0.0.1", String(tcp.listener.port)], + cwd: dir, + }) + expect(result.code).toBe(0) + expect(result.stdout).toBe("sandbox-tcp-ok") + expect(tcp.accepted()).toBe(1) + } finally { + tcp.listener.stop(true) + } + }) + + mac("allows a sandboxed child to listen for inbound loopback traffic in deny mode", async () => { + const dir = await root() + const probe = server("127.0.0.1") + const port = probe.listener.port + probe.listener.stop(true) + const target = await prepareLaunch(profile(dir, "deny"), { + command: "/usr/bin/nc", + args: ["-l", "127.0.0.1", String(port)], + cwd: dir, + }) + const child = Bun.spawn([target.command, ...target.args], { + cwd: target.cwd, + env: target.environment, + stdout: "pipe", + stderr: "pipe", + }) + const timeout = setTimeout(() => child.kill(), 5_000) + try { + const connected = await (async () => { + for (const _ of Array.from({ length: 100 })) { + const socket = await Bun.connect({ + hostname: "127.0.0.1", + port, + socket: { + open(socket) { + socket.write("sandbox-inbound-ok") + socket.end() + }, + data() {}, + error() {}, + }, + }).catch(() => undefined) + if (socket) return true + await Bun.sleep(20) + } + return false + })() + const [code, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + expect(connected).toBe(true) + expect(code).toBe(0) + expect(stdout).toBe("sandbox-inbound-ok") + expect(stderr).toBe("") + } finally { + clearTimeout(timeout) + child.kill() + } + }) + + mac("denies a sandboxed child loopback TCP connection with a kernel permission error", async () => { + const dir = await root() + const tcp = server("127.0.0.1") + try { + const result = await launch(profile(dir, "deny"), { + command: "/usr/bin/nc", + args: ["-v", "127.0.0.1", String(tcp.listener.port)], + cwd: dir, + }) + expect(result.code).not.toBe(0) + expect(result.stderr).toContain("Operation not permitted") + expect(tcp.accepted()).toBe(0) + } finally { + tcp.listener.stop(true) + } + }) + + mac("enforces allow and deny modes for child HTTP requests", async () => { + const dir = await root() + const allowed = http() + const denied = http() + try { + const allow = await launch(profile(dir, "allow"), { + command: "/usr/bin/curl", + args: ["--noproxy", "*", "-fsS", allowed.server.url.toString()], + cwd: dir, + }) + const deny = await launch(profile(dir, "deny"), { + command: "/usr/bin/curl", + args: ["--noproxy", "*", "-fsS", denied.server.url.toString()], + cwd: dir, + }) + expect(allow.code).toBe(0) + expect(allow.stdout).toBe("sandbox-http-ok") + expect(allowed.requests()).toBe(1) + expect(deny.code).not.toBe(0) + expect(denied.requests()).toBe(0) + } finally { + await Promise.all([allowed.server.stop(true), denied.server.stop(true)]) + } + }) + + mac("denies hostname and IPv6 loopback forms", async () => { + const dir = await root() + const ipv4 = server("127.0.0.1") + const ipv6 = server("::1") + try { + const localhost = await launch(profile(dir, "deny"), { + command: "/usr/bin/nc", + args: ["-v", "localhost", String(ipv4.listener.port)], + cwd: dir, + }) + const direct = await launch(profile(dir, "deny"), { + command: "/usr/bin/nc", + args: ["-v", "::1", String(ipv6.listener.port)], + cwd: dir, + }) + expect(localhost.code).not.toBe(0) + expect(localhost.stderr).toContain("Operation not permitted") + expect(direct.code).not.toBe(0) + expect(direct.stderr).toContain("Operation not permitted") + expect(ipv4.accepted()).toBe(0) + expect(ipv6.accepted()).toBe(0) + } finally { + ipv4.listener.stop(true) + ipv6.listener.stop(true) + } + }) + + for (const mode of ["allow", "deny"] as const) { + mac(`preserves filesystem policy in ${mode} network mode`, async () => { + const dir = await root() + const outside = join(await root(), `${mode}.txt`) + const allowed = join(dir, `${mode}.txt`) + const blocked = join(dir, ".protected", `${mode}.txt`) + const write = await launch(profile(dir, mode), { + command: "/bin/sh", + args: ["-c", 'printf allowed > "$1"', "sandbox-write", allowed], + cwd: dir, + }) + const deny = await launch(profile(dir, mode), { + command: "/bin/sh", + args: ["-c", 'printf blocked > "$1"', "sandbox-write", blocked], + cwd: dir, + }) + const escape = await launch(profile(dir, mode), { + command: "/bin/sh", + args: ["-c", 'printf blocked > "$1"', "sandbox-write", outside], + cwd: dir, + }) + expect(write.code).toBe(0) + expect(await readFile(allowed, "utf8")).toBe("allowed") + expect(deny.code).not.toBe(0) + expect(await Bun.file(blocked).exists()).toBe(false) + expect(escape.code).not.toBe(0) + expect(await Bun.file(outside).exists()).toBe(false) + }) + } +}) diff --git a/packages/kilo-sandbox/tsconfig.json b/packages/kilo-sandbox/tsconfig.json new file mode 100644 index 00000000000..a495f567aaa --- /dev/null +++ b/packages/kilo-sandbox/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": ["bun"], + "noUncheckedIndexedAccess": false + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 7085b54750d..4ab768a0fee 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index b68c0fcf20b..e30fbb37c0f 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index 0111b6381a9..49ed771af57 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -129,19 +129,62 @@ html[data-theme="kilo-vscode"] [data-component="tool-trigger"] { } } +@keyframes tool-card-down { + from { + grid-template-rows: 0fr; + opacity: 0; + } + + to { + grid-template-rows: 1fr; + opacity: 1; + } +} + +@keyframes tool-card-up { + from { + grid-template-rows: 1fr; + opacity: 1; + } + + to { + grid-template-rows: 0fr; + opacity: 0; + } +} + /* Card styling for tool calls in the VS Code sidebar theme */ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] { border-radius: 0; border: 0; overflow: visible; - /* Header trigger — add bottom border when the collapsible is open */ [data-component="collapsible"].tool-collapsible { gap: 0; border-radius: 0; - &:has([data-slot="collapsible-trigger"][aria-expanded="true"]) { - gap: 6px; + > [data-slot="collapsible-content"] { + display: grid; + overflow: visible; + clip-path: inset(0 -6px); + + > * { + min-height: 0; + } + } + + > [data-slot="collapsible-content"][data-expanded] { + animation: tool-card-down 220ms cubic-bezier(0.23, 1, 0.32, 1); + } + + > [data-slot="collapsible-content"][data-closed] { + animation: tool-card-up 160ms cubic-bezier(0.77, 0, 0.175, 1); + } + + [data-slot="basic-tool-details"]::before { + display: block; + height: 6px; + content: ""; } [data-slot="collapsible-trigger"] { @@ -409,3 +452,12 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty } } } + +@media (prefers-reduced-motion: reduce) { + html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] { + [data-component="collapsible"].tool-collapsible > [data-slot="collapsible-content"][data-expanded], + [data-component="collapsible"].tool-collapsible > [data-slot="collapsible-content"][data-closed] { + animation-duration: 0.01ms; + } + } +} diff --git a/packages/kilo-ui/src/components/basic-tool.tsx b/packages/kilo-ui/src/components/basic-tool.tsx index 0f4234ad1a4..7ba072c7c44 100644 --- a/packages/kilo-ui/src/components/basic-tool.tsx +++ b/packages/kilo-ui/src/components/basic-tool.tsx @@ -20,14 +20,16 @@ export function initialOpen(props: OpenProps) { export function BasicTool(props: BasicToolProps) { const key = () => toolOpenKey(props) const initial = () => initialOpen(props) + const change = (open: boolean) => { + writeToolOpen(key(), open) + props.onOpenChange?.(open) + } + if (!("children" in props)) { + return + } return ( - { - writeToolOpen(key(), open) - props.onOpenChange?.(open) - }} - /> + +
{props.children}
+ ) } diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index ffb4ae84ec4..5a3fd732da0 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -477,6 +477,30 @@ html[data-theme="kilo-vscode"] [data-component="todos"] { padding: 8px; } +@keyframes reasoning-card-down { + from { + grid-template-rows: 0fr; + opacity: 0; + } + + to { + grid-template-rows: 1fr; + opacity: 1; + } +} + +@keyframes reasoning-card-up { + from { + grid-template-rows: 1fr; + opacity: 1; + } + + to { + grid-template-rows: 0fr; + opacity: 0; + } +} + /* Reasoning block: visually distinct from normal agent output. Reasoning stays open unless the user explicitly collapses it. */ [data-component="reasoning-part"] { @@ -550,22 +574,21 @@ html[data-theme="kilo-vscode"] [data-component="todos"] { } } - /* Collapse/expand animation via Kobalte's CSS variable. - Uses grid trick: the outer wrapper transitions grid-template-rows - between 0fr (collapsed) and 1fr (expanded) so the content smoothly - collapses without needing to know the pixel height up front. */ [data-slot="collapsible-content"] { display: grid; - grid-template-rows: 0fr; - transition: grid-template-rows 250ms ease-out; overflow: hidden; > * { + min-height: 0; overflow: hidden; } &[data-expanded] { - grid-template-rows: 1fr; + animation: reasoning-card-down 220ms cubic-bezier(0.23, 1, 0.32, 1); + } + + &[data-closed] { + animation: reasoning-card-up 160ms cubic-bezier(0.77, 0, 0.175, 1); } } @@ -598,10 +621,6 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] { [data-component="collapsible"].tool-collapsible { gap: 0; border-radius: 0; - - &:has([data-slot="collapsible-trigger"][aria-expanded="true"]) { - gap: 6px; - } } [data-slot="collapsible-trigger"] { @@ -667,6 +686,12 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] { width: calc(100% + 12px); } + [data-slot="reasoning-details"]::before { + display: block; + height: 6px; + content: ""; + } + [data-slot="reasoning-content"] { padding: 8px 12px 10px; border: 0; @@ -712,3 +737,10 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] { line-height: var(--line-height-large); } } + +@media (prefers-reduced-motion: reduce) { + [data-component="reasoning-part"] [data-slot="collapsible-content"][data-expanded], + [data-component="reasoning-part"] [data-slot="collapsible-content"][data-closed] { + animation-duration: 0.01ms; + } +} diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 9f8b88cb758..4fb2aab53bc 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -770,7 +770,7 @@ export function UserMessageDisplay(props: { const providerID = props.message.model?.providerID const modelID = props.message.model?.modelID if (!providerID || !modelID) return "" - const match = data.store.provider?.all?.find((p) => p.id === providerID) + const match = data.store.provider?.all?.get(providerID) return match?.models?.[modelID]?.name ?? modelID }) @@ -1591,8 +1591,10 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp -
- +
+
+ +
@@ -2235,7 +2237,6 @@ ToolRegistry.register({ defaultOpen={props.defaultOpen ?? true} onOpenChange={setOpen} allowPendingToggle - animated trigger={
diff --git a/packages/kilo-ui/src/pierre/index.test.ts b/packages/kilo-ui/src/pierre/index.test.ts index 5f2df0e9518..7bc73fd3a24 100644 --- a/packages/kilo-ui/src/pierre/index.test.ts +++ b/packages/kilo-ui/src/pierre/index.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test" import { createDefaultOptions } from "./index" describe("Pierre diff options", () => { - test("highlights changed characters in unified and split diffs", () => { - expect(createDefaultOptions("unified").lineDiffType).toBe("char") - expect(createDefaultOptions("split").lineDiffType).toBe("char") + test("keeps changed identifiers intact in unified and split diffs", () => { + expect(createDefaultOptions("unified").lineDiffType).toBe("word-alt") + expect(createDefaultOptions("split").lineDiffType).toBe("word-alt") }) }) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 1df03fb88d2..7b4b13aabe9 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -10,7 +10,9 @@ import { createDefaultOptions as defaults, styleVariables } from "@opencode-ai/u export { styleVariables } -export const LINE_DIFF_TYPE = "char" as const +// Character matching fragments inserted identifiers when they share letters with +// existing symbols. Word-alt keeps those logical additions visually intact. +export const LINE_DIFF_TYPE = "word-alt" as const // Pierre 1.1 treats its changed-line override properties as tint targets. Apply // Kilo semantic surfaces at the computed row level so host diff colors stay final. diff --git a/packages/kilo-vscode/.vscodeignore b/packages/kilo-vscode/.vscodeignore index b8e0d54b256..157f9841d78 100644 --- a/packages/kilo-vscode/.vscodeignore +++ b/packages/kilo-vscode/.vscodeignore @@ -25,6 +25,10 @@ AGENTS.md # Include bin/ directory for CLI binary (production) !bin/** +# .cli-version is a local-source build marker written by local-bin.ts. It must +# not ship in the VSIX, otherwise production installs are detected as local +# builds and may inject a dev-only bwrap fallback (see ServerManager.localCli). +bin/.cli-version # Include WAV assets used by extension-host notification playback !audio-wav/** diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index d7c983f454c..9247ea6e3d0 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -1,5 +1,272 @@ # kilo-code +## 7.3.54 + +## 7.3.53 + +### Patch Changes + +- [#11533](https://github.com/Kilo-Org/kilocode/pull/11533) [`15f42d4`](https://github.com/Kilo-Org/kilocode/commit/15f42d4bec51bbb127636738275f36fdc07e7b33) - Restore bounded text-file reads and keep zero-limit pagination and Unicode truncation from producing unusable tool output. + +- Updated dependencies [[`6c55c28`](https://github.com/Kilo-Org/kilocode/commit/6c55c28ec345a6d90d2d7a4e345abf962f208e29)]: + - @kilocode/kilo-gateway@7.3.53 + - @kilocode/kilo-indexing@7.3.53 + - @kilocode/kilo-ui@7.3.53 + - @opencode-ai/ui@7.3.53 + +## 7.3.52 + +### Patch Changes + +- [#11450](https://github.com/Kilo-Org/kilocode/pull/11450) [`cc924a6`](https://github.com/Kilo-Org/kilocode/commit/cc924a67d9b190ccffebaefa983213e173db54d8) - Changes from opencode v1.15.9 to v1.15.13 upstream: + - Core Improvements: Added `headerTimeout` config for provider requests, with a 10s default for default OpenAI setups. + - Core Improvements: Experimental background agents now push updates without polling. + - Core Improvements: You can now set only `modalities.input` or `modalities.output` in config. (@robposch) + - Core Improvements: Remote-backed projects now resolve a stable project identity. + - Core Improvements: ACP integrations can now send prompts, slash commands, and usage updates through `acp-next` + - Core Improvements: Added WebSocket transport for OpenAI responses on supported channels (set KILO_EXPERIMENTAL_WEBSOCKETS=true) + - Core Improvements: Sessions can now store custom metadata through the API and SDK. (@shantur) + - Core Improvements: Config now loads from the opened location upward, so directory-specific settings and provider policies apply more predictably. + - Core Bugfixes: Dynamically added MCP servers now disconnect cleanly when removed. + - Core Bugfixes: DigitalOcean inference now uses your OAuth token directly instead of creating a MAK. (@Spherrrical) + - Core Bugfixes: Config loading now falls back cleanly when user info is unavailable. + - Core Bugfixes: Fixed Google tool calling after the upstream tool ID regression. + - Core Bugfixes: Experimental flags can now override the umbrella experimental flag. + - Core Bugfixes: Resumed sessions no longer continue orphaned interrupted tools. (@edevil) + - Core Bugfixes: OpenAI reasoning summaries now render as separate blocks. + - Core Bugfixes: Updated Google Vertex support for reasoning signatures. + - Core Bugfixes: The shell tool now advertises your configured timeout to the model. + - Core Bugfixes: Enabled adaptive reasoning controls for Anthropic Opus 4.7+ models + - Core Bugfixes: Allowed colons in passwords (@neriousy) + - Core Bugfixes: Sped up warm `acp-next` model and config switches + - Core Bugfixes: Improved first-session `acp-next` startup time + - Core Bugfixes: Kept OpenAI WebSocket response timeouts active + - Core Bugfixes: Retried failed OpenAI WebSocket streams before falling back + - Core Bugfixes: Handled `acp-next` permission prompts correctly + - Core Bugfixes: Used the persisted session directory for existing-session requests + - Core Bugfixes: Forwarded remote workspace request bodies correctly + - Core Bugfixes: Supported custom base URLs for OpenAI WebSocket responses (@Tarquinen) + - Core Bugfixes: Gateway Anthropic Opus 4.7+ adaptive reasoning now keeps summarized thinking instead of returning empty thinking blocks. + - TUI Improvements: Made the prompt resize with terminal width and added prompt size config. (@bjschafer) + - TUI Improvements: Added a workspace management dialog + - TUI Bugfixes: Accelerated diff viewer scrolling. + - TUI Bugfixes: External editors now open from the worktree directory when available. + - TUI Bugfixes: Kept session navigation working while prompt modes are open + - TUI Bugfixes: Restored the thinking spinner + - TUI Bugfixes: Surfaced subagent retry status + - TUI Bugfixes: Fixed opening editors from non-Git project paths (@OpeOginni) + - TUI Bugfixes: Wrapped inline tool rows now stay aligned, and failed inline tools can expand their error details in place. + - Extensions Improvements: Added a `dispose` hook for plugins. + - Extensions Bugfixes: Fixed Codex plugin requests to send the expected session ID header. + +## 7.3.51 + +## 7.3.50 + +### Minor Changes + +- [#11421](https://github.com/Kilo-Org/kilocode/pull/11421) [`ccec216`](https://github.com/Kilo-Org/kilocode/commit/ccec2162383a6f378ed5e62d630720607d185209) - Show a BYOK badge for Kilo Gateway models that can use an enabled personal or organization provider key. + +### Patch Changes + +- [#11418](https://github.com/Kilo-Org/kilocode/pull/11418) [`8b32375`](https://github.com/Kilo-Org/kilocode/commit/8b32375fe67d96f29fa88933d711699e3720ebf4) - Describe custom providers independently of their selected API protocol. + +- [#11455](https://github.com/Kilo-Org/kilocode/pull/11455) [`4d09333`](https://github.com/Kilo-Org/kilocode/commit/4d0933371ca9be212cdd0357605e250ebacf7e1b) - Hide reverted provider errors so Redo controls remain visible after rewinding a session. + +- [#11423](https://github.com/Kilo-Org/kilocode/pull/11423) [`aa17a8a`](https://github.com/Kilo-Org/kilocode/commit/aa17a8a4191a04604b3a402e75d5c7e7b8149da4) - Keep changed identifiers intact when highlighting edits within diff lines. + +- [#11453](https://github.com/Kilo-Org/kilocode/pull/11453) [`f7e68d1`](https://github.com/Kilo-Org/kilocode/commit/f7e68d19d9d8b23b087d3c7c92d487abced8d7ec) - Limit completion sounds to parent agent sessions. + +- [#10940](https://github.com/Kilo-Org/kilocode/pull/10940) [`4de8293`](https://github.com/Kilo-Org/kilocode/commit/4de82931ed3d5315e6717827a9b53b11c1162e7f) Thanks [@mjnaderi](https://github.com/mjnaderi)! - Avoid failing Agent Manager startup when another extension already registered VS Code panel commands. + +- [#11464](https://github.com/Kilo-Org/kilocode/pull/11464) [`c829642`](https://github.com/Kilo-Org/kilocode/commit/c8296420544a68e730fa4307e1a045210d79fcad) - Keep chat output updating after reverting and resubmitting a prompt. + +- [#11433](https://github.com/Kilo-Org/kilocode/pull/11433) [`867beac`](https://github.com/Kilo-Org/kilocode/commit/867beac3a19881861536d6c22a9efcb5ae379cc4) - Prevent concurrent subagent updates from blanking the Agent Manager webview. + +- [#11463](https://github.com/Kilo-Org/kilocode/pull/11463) [`909ec73`](https://github.com/Kilo-Org/kilocode/commit/909ec73b9c852de6899c20f473b6da14d6c428b5) - Speed up Agent Manager worktree hover cards so pull request details appear and dismiss more quickly. + +- [#11451](https://github.com/Kilo-Org/kilocode/pull/11451) [`732fbc3`](https://github.com/Kilo-Org/kilocode/commit/732fbc30dc3ee969caabc96282ad1a0ff5c652ce) - Widen the chat readable lane from 88ch to 98ch so conversations, tools, and diffs can use more editor space. + +- Updated dependencies [[`ccec216`](https://github.com/Kilo-Org/kilocode/commit/ccec2162383a6f378ed5e62d630720607d185209), [`2c9e72c`](https://github.com/Kilo-Org/kilocode/commit/2c9e72c14a87387199fd42546746bbea30aa1570), [`f7e68d1`](https://github.com/Kilo-Org/kilocode/commit/f7e68d19d9d8b23b087d3c7c92d487abced8d7ec)]: + - @kilocode/kilo-gateway@7.4.0 + - @kilocode/sdk@7.3.50 + - @kilocode/kilo-indexing@7.3.50 + - @kilocode/kilo-ui@7.3.50 + - @kilocode/plugin@7.3.50 + - @opencode-ai/ui@7.3.50 + +## 7.3.49 + +### Minor Changes + +- [#11303](https://github.com/Kilo-Org/kilocode/pull/11303) [`6faa3f1`](https://github.com/Kilo-Org/kilocode/commit/6faa3f14b619144a1353f169a0a2855dcbf69bbb) - Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard. + +## 7.3.48 + +### Minor Changes + +- [#11239](https://github.com/Kilo-Org/kilocode/pull/11239) [`a5b87fa`](https://github.com/Kilo-Org/kilocode/commit/a5b87fafa37f223877d07cfba2882b298a25f742) - Support multiple provider APIs, adaptive thinking, split reasoning, and output effort variants for custom providers. + +- [#11331](https://github.com/Kilo-Org/kilocode/pull/11331) [`c5bd1b3`](https://github.com/Kilo-Org/kilocode/commit/c5bd1b3e0959e019b015ca73c8db71726a5ffba3) - Support closing other Agent Manager tabs from the tab context menu. + +- [#11098](https://github.com/Kilo-Org/kilocode/pull/11098) [`b0ec91c`](https://github.com/Kilo-Org/kilocode/commit/b0ec91cef439633eef955d44747e03bb5e15ee4f) - Offer opt-in attention sounds when sessions finish, error, or need input across the sidebar, editor tabs, and Agent Manager. + +- [#11367](https://github.com/Kilo-Org/kilocode/pull/11367) [`1933cf7`](https://github.com/Kilo-Org/kilocode/commit/1933cf7cab911e295b99cb0ecf8821312e47c23e) - Open the selected Agent Manager worktree's pull request with Cmd/Ctrl+Shift+R. + +- [#11308](https://github.com/Kilo-Org/kilocode/pull/11308) [`d56bfdd`](https://github.com/Kilo-Org/kilocode/commit/d56bfdd9aa8c3112550be11f4761c66918283dde) - Keep conversations readable across the sidebar, editor tabs, and Agent Manager by aligning messages, tool output, and the composer in one centered lane. + +- [#11258](https://github.com/Kilo-Org/kilocode/pull/11258) [`056e069`](https://github.com/Kilo-Org/kilocode/commit/056e06911b0bf4232021e8a56676ae9b3f888bb0) - Render image changes in diff viewers and open images with VS Code's image preview. + +- [#11294](https://github.com/Kilo-Org/kilocode/pull/11294) [`c961f66`](https://github.com/Kilo-Org/kilocode/commit/c961f66bd4f38c922640d721d1ebdbcc9f97c0a6) - Search and switch between local sessions, worktrees, and their sessions from the Agent Manager sidebar. + +- [#11317](https://github.com/Kilo-Org/kilocode/pull/11317) [`a2ccf57`](https://github.com/Kilo-Org/kilocode/commit/a2ccf574a7b3f1414b9e9d785265dc9c4d16b44e) - Support VS Code switch links that select a Kilo model, agent, or both. + +### Patch Changes + +- [#11242](https://github.com/Kilo-Org/kilocode/pull/11242) [`9211000`](https://github.com/Kilo-Org/kilocode/commit/9211000aadd909f0d46746604c3e963966a59660) - Support unauthenticated OpenAI-compatible endpoints for codebase indexing without requiring a placeholder API key. + +- [#11249](https://github.com/Kilo-Org/kilocode/pull/11249) [`2c30dc7`](https://github.com/Kilo-Org/kilocode/commit/2c30dc75ce18c018f603a30d1c9e3c70fe8fc036) - Show a clear, retryable provider rate-limit error instead of raw response JSON in chat. + +- [#11080](https://github.com/Kilo-Org/kilocode/pull/11080) [`e335f97`](https://github.com/Kilo-Org/kilocode/commit/e335f97856c2a13ab75c2cb6e24f7df2626d41a0) Thanks [@Githubguy132010](https://github.com/Githubguy132010)! - Support opening code edit and diff blocks expanded by default in the VS Code chat. + +- [#11297](https://github.com/Kilo-Org/kilocode/pull/11297) [`06da363`](https://github.com/Kilo-Org/kilocode/commit/06da3635993c17053d33cfd64d5002f3868eba11) - Remove stray rounded corners between files in the Changes diff viewer. + +- [#11352](https://github.com/Kilo-Org/kilocode/pull/11352) [`5dd5c9a`](https://github.com/Kilo-Org/kilocode/commit/5dd5c9a0de5d92b1646c647e3efbeeee29937721) - Improve screen reader navigation and provider group controls in the model picker. + +- [#10788](https://github.com/Kilo-Org/kilocode/pull/10788) [`941007b`](https://github.com/Kilo-Org/kilocode/commit/941007b68dfa9500ad604620ae861accf2a74643) Thanks [@noobezlol](https://github.com/noobezlol)! - Handle string-form permission values when migrating bash permissions. + +- [#11158](https://github.com/Kilo-Org/kilocode/pull/11158) [`8ff8371`](https://github.com/Kilo-Org/kilocode/commit/8ff83711766ff6b18ea23d1990d6fedd8e79c5ae) - Add a shared model setting to hide Kilo Gateway models that may train on your prompts across Kilo clients. + +- [#11349](https://github.com/Kilo-Org/kilocode/pull/11349) [`a5395a3`](https://github.com/Kilo-Org/kilocode/commit/a5395a35210b92d658b8efacea0fb9d25ddb27d0) - Highlight the exact changed characters within modified diff lines. + +- [#10261](https://github.com/Kilo-Org/kilocode/pull/10261) [`8efc296`](https://github.com/Kilo-Org/kilocode/commit/8efc296ae54292183579e004eae52813a96abe0d) Thanks [@singhvishalkr](https://github.com/singhvishalkr)! - Ignore Enter while IME composition is active (including Windows virtual-key 229) so composed text is not accidentally submitted from chat and sidebar inputs. + +- [#11236](https://github.com/Kilo-Org/kilocode/pull/11236) [`1511d13`](https://github.com/Kilo-Org/kilocode/commit/1511d13b3f7f20001d2111f14bdfae7155372cf8) Thanks [@kapelame](https://github.com/kapelame)! - Add an instant/thinking reasoning toggle for MiniMax M-series models, matching the existing glm/kimi/qwen behavior. + +- [#11358](https://github.com/Kilo-Org/kilocode/pull/11358) [`0920b3f`](https://github.com/Kilo-Org/kilocode/commit/0920b3ffa8ec5f7b327681f446af1616d6d652a2) - Clean up incomplete worktrees when moving a session fails to transfer its Git changes. + +- [#11348](https://github.com/Kilo-Org/kilocode/pull/11348) [`d190725`](https://github.com/Kilo-Org/kilocode/commit/d190725c3cfd30741aa0e19dac3ee7d626654bd1) - Announce every Settings tab by name to screen readers in compact sidebars. + +- [#11257](https://github.com/Kilo-Org/kilocode/pull/11257) [`f42789d`](https://github.com/Kilo-Org/kilocode/commit/f42789d0ef5585aad4080bdc5c96856675cd9503) - Changes from opencode v1.14.51 to v1.15.4 upstream: + - Core Improvements: Clarified how to recover when the npm package is installed without its native binary. + - Core Improvements: Reduced unnecessary prompting around shell, task, and todo flows. + - Core Bugfixes: Ignored invalid exports in custom tool modules instead of failing tool loading. + - Core Bugfixes: Ignored project instruction lookup errors so sessions keep loading when project instruction discovery fails. + - Core Bugfixes: Fixed versioned event projector lookups so event replay uses the right handlers. + - Core Bugfixes: Avoid duplicate consecutive entries in prompt history. + - Core Bugfixes: Show full config validation errors during TUI startup instead of a generic failure. + - Core Bugfixes: Fixed npm installs so the CLI can recover and fetch the right native binary on more setups. + - Core Bugfixes: Fixed multiline `@` mentions in prompts. + - Core Bugfixes: Preserved custom tool metadata from Zod schemas. + - Core Bugfixes: Preserved custom tool argument descriptions in generated schemas. + - Core Bugfixes: Fixed file watching in repos where `.git` is a symlink. (@kagura-agent) + - Core Bugfixes: Fixed sync events not reaching project-scoped subscribers in injected instances. + - Core Bugfixes: Reduced wasted work when reading very large files after output truncation. + - Core Bugfixes: Fixed project-scoped bus events so file watcher and update notifications reach the right instance. + - Core Bugfixes: Fixed custom LSP servers not sending refresh events after they initialize. + - Core Bugfixes: Hid background subagent task instructions unless experimental background mode is enabled. + - TUI Improvements: Added a collapsed thinking view that can be expanded inline. + - TUI Improvements: Added pinned sessions with quick-switch slots in the session picker. + - TUI Improvements: Newly pinned sessions now stay at the end of the pinned list instead of jumping to the top. + - TUI Improvements: Made Markdown H1 headings easier to distinguish. + - TUI Bugfixes: Fixed thinking mode defaults so reasoning starts collapsed consistently. + - TUI Bugfixes: Limited session quick-switching to pinned sessions. + - TUI Bugfixes: Fixed Markdown table rendering in chat output. + - TUI Bugfixes: Fixed `kilo run --agent` resolving project-local agents. + - TUI Bugfixes: Fixed async commands losing the active instance context, which could break agent generation and GitHub-driven runs. + +- [#11356](https://github.com/Kilo-Org/kilocode/pull/11356) [`326ff35`](https://github.com/Kilo-Org/kilocode/commit/326ff351460342f93b0bf97f0beb6383357c5d05) - Changes from opencode v1.15.4 to v1.15.9 upstream: + - Core Improvements: Preview the native OpenAI runtime path behind an experimental flag + - Core Improvements: Add `--replay` and `--replay-limit` to show recent history when resuming interactive runs + - Core Improvements: Added a diff viewer in the TUI for reviewing changes. + - Core Improvements: Collapsed single-child directories in the diff viewer file tree. + - Core Improvements: Added shell mode to the `run` prompt. + - Core Improvements: Replaced subagent tabs with an on-demand picker in `run`. + - Core Improvements: Plugin file load errors no longer break the rest of plugin loading. + - Core Improvements: Anthropic API-key models now use the native runtime. + - Core Improvements: The v2 HTTP API now exposes structured public error schemas. + - Core Improvements: Added Grok OAuth sign-in, including device-code login. (@Jaaneek) + - Core Improvements: Redesigned the diff viewer with a file tree and refreshed layout. + - Core Bugfixes: Fix plugin tools using `ask` so tool calls complete correctly + - Core Bugfixes: Reduce missed `/event` updates caused by a subscription race + - Core Bugfixes: Sort the v2 session list by most recently updated + - Core Bugfixes: Zed editor context now only activates inside Zed terminals. + - Core Bugfixes: Agent and command names now resolve correctly from relative config paths. + - Core Bugfixes: Invalid `KILO_PERMISSION` JSON no longer crashes startup. + - Core Bugfixes: Plugin tools with missing `args` no longer break tool loading. + - Core Bugfixes: Restored legacy `PgUp` and `PgDn` TUI keybind aliases. + - Core Bugfixes: Native runtime now prefers the console provider token for OpenCode models. + - Core Bugfixes: V2 session APIs now return safe `UnknownError` responses with log reference IDs when stored messages are corrupt. + - Core Bugfixes: Generic API 500s no longer expose config details from server errors. + - Core Bugfixes: Unknown API errors now include reference IDs so you can match responses to server logs. + - Core Bugfixes: V2 session APIs now return `503 ServiceUnavailableError` for mutations that are not available yet. + - Core Bugfixes: V2 session APIs now return `SessionNotFoundError` for missing sessions. + - Core Bugfixes: Deduped concurrent Codex OAuth refreshes to avoid repeated refresh failures. (@cooper-oai) + - Core Bugfixes: Restored native OpenAI OAuth requests. + - Core Bugfixes: Tool schema failures now surface as friendly tool errors. + - Core Bugfixes: Added PDF attachment support for Grok. + - Core Bugfixes: Restored OpenAI reasoning streams. + - Core Bugfixes: Return to the previous screen when closing the diff viewer. + - Core Bugfixes: Show clearer errors when a default model is invalid or unavailable. + - Core Bugfixes: Surface missing PTY session errors instead of failing generically. + - Core Bugfixes: Improve diff viewer empty states and context handling. + - Core Bugfixes: Show clearer errors when a skill invocation fails as expected. + - Core Bugfixes: Show clearer errors when an installation upgrade fails. + - Core Bugfixes: Show clearer project not found errors from the HTTP API. + - Core Bugfixes: Return PTY error bodies from the HTTP API. + - Core Bugfixes: Enable the diff viewer by default. + - Core Bugfixes: Return MCP server not found errors from the HTTP API. + - Core Bugfixes: Let MCP OAuth configs set a callback port and include configured scopes in client metadata. (@sebin) + - Core Bugfixes: Use working Vertex Anthropic endpoints for `us` and `eu` multi-region setups. (@JPFrancoia) + - Core Bugfixes: Return session busy error bodies from the HTTP API. + - Core Bugfixes: Preserve native reasoning continuation metadata across turns. + - TUI Improvements: Refresh the prompt layout after pasting content + - TUI Improvements: The diff viewer now focuses the first file automatically. + - TUI Improvements: Copy the current worktree path from the command palette. + - TUI Bugfixes: Keep file references scoped to the current workspace + - TUI Bugfixes: Preserve pasted prompt content when copying + - TUI Bugfixes: Collapse very long tool output lines to keep the layout readable + - TUI Bugfixes: Use a higher-contrast paste summary badge color in some themes (@kagura-agent) + - TUI Bugfixes: Imported sessions now refresh their directory and relative path fields correctly. (@OpeOginni) + - TUI Bugfixes: Collapsed thinking labels now use clearer punctuation. + - TUI Bugfixes: New sessions now default to the local project. + - TUI Bugfixes: Single-select question checkmarks no longer run into option labels. + - TUI Bugfixes: Refine diff viewer keyboard shortcuts. + - TUI Bugfixes: Restore question prompt key handling. + - TUI Bugfixes: Keep the spinner color aligned with the active agent. (@OpeOginni) + +- [#11146](https://github.com/Kilo-Org/kilocode/pull/11146) [`481f836`](https://github.com/Kilo-Org/kilocode/commit/481f8361cfc5ea4be929d9afd63d996ed51131af) Thanks [@Drixled](https://github.com/Drixled)! - Polish chat tool-call previews for shell, search, and background-process output. + +- [#11373](https://github.com/Kilo-Org/kilocode/pull/11373) [`f21a34a`](https://github.com/Kilo-Org/kilocode/commit/f21a34a1e63107da085eb9e57172ca6025d2dbe0) - Skip attention sounds when a session is manually interrupted. + +- [#11351](https://github.com/Kilo-Org/kilocode/pull/11351) [`94bdfb9`](https://github.com/Kilo-Org/kilocode/commit/94bdfb9f8e4d88205f876e7936e10908c68a8b44) - Enable remote control from Agent Manager without disrupting concurrent sidebar sessions, and include all open Agent Manager sessions. + +- [#11318](https://github.com/Kilo-Org/kilocode/pull/11318) [`7f18708`](https://github.com/Kilo-Org/kilocode/commit/7f18708308ad47fd14f876ed4a25c4322bd29751) - Remove the divider between agent conversations and the prompt input across extension chat views. + +- [#11295](https://github.com/Kilo-Org/kilocode/pull/11295) [`2fa0890`](https://github.com/Kilo-Org/kilocode/commit/2fa0890928f7dd060125ad4f4083b8bd2bf3e69b) - Restore speech input when profile details are unavailable, move transcription model selection to the Models tab, and default transcription to Whisper Large V3 Turbo. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`9b73bf9`](https://github.com/Kilo-Org/kilocode/commit/9b73bf91e4303249f30517d4cacfa8287680a82c) - Restore popular provider suggestions in the VS Code provider settings panel when provider metadata is unavailable. + +- [#11410](https://github.com/Kilo-Org/kilocode/pull/11410) [`344a6a5`](https://github.com/Kilo-Org/kilocode/commit/344a6a5f0f8377d8ab38792e6141d08947a7dc19) - Keep server controls and events connected to active sessions and subagents. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`987da27`](https://github.com/Kilo-Org/kilocode/commit/987da2728731e1da1c974996b5bcddafe745cea7) - Show shared provider descriptions and provider icons in JetBrains and VS Code provider settings. + +- [#11357](https://github.com/Kilo-Org/kilocode/pull/11357) [`14cb3df`](https://github.com/Kilo-Org/kilocode/commit/14cb3dfd048ba4217bcd1f26ad7b76ce9a2a2e7f) - Show line numbers in edit approval diffs, including compact sidebar views. + +- [#11402](https://github.com/Kilo-Org/kilocode/pull/11402) [`146c4f4`](https://github.com/Kilo-Org/kilocode/commit/146c4f4396fd45c6bfc567f9b3ee1de46e26a3b6) - Prevent large highlighted shell outputs from blanking the VS Code webview when expanded. + +- [#11404](https://github.com/Kilo-Org/kilocode/pull/11404) [`f35d9c5`](https://github.com/Kilo-Org/kilocode/commit/f35d9c5fa3017dcc35d260b5669daf87411419f5) - Keep the Stop button working when an active session moves between the workspace and an Agent Manager worktree. + +- [#11366](https://github.com/Kilo-Org/kilocode/pull/11366) [`4739954`](https://github.com/Kilo-Org/kilocode/commit/47399547e1d60f016542f6a83fbf91a2bce91692) - Widen the sidebar chat readable lane from 78ch to 88ch so tools and content use a little more screen space. + +- Updated dependencies [[`9211000`](https://github.com/Kilo-Org/kilocode/commit/9211000aadd909f0d46746604c3e963966a59660), [`2fa0890`](https://github.com/Kilo-Org/kilocode/commit/2fa0890928f7dd060125ad4f4083b8bd2bf3e69b), [`973d02c`](https://github.com/Kilo-Org/kilocode/commit/973d02cfd15b3bf3eefefe92e7fb61059eba26f7), [`66af690`](https://github.com/Kilo-Org/kilocode/commit/66af6907005b99bb39a0869b35dfe1ec180cc0b5)]: + - @kilocode/kilo-indexing@7.4.0 + - @kilocode/sdk@7.4.0 + - @kilocode/kilo-ui@7.3.47 + - @kilocode/plugin@7.3.47 + - @opencode-ai/ui@7.3.47 + - @kilocode/kilo-gateway@7.3.47 + ## 7.3.46 ### Patch Changes diff --git a/packages/kilo-vscode/THIRD_PARTY_LICENSES/continue.txt b/packages/kilo-vscode/THIRD_PARTY_LICENSES/continue.txt new file mode 100644 index 00000000000..9aea05f9b72 --- /dev/null +++ b/packages/kilo-vscode/THIRD_PARTY_LICENSES/continue.txt @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all other +commercial damages or losses), even if such Contributor has been +advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright 2023 Continue Dev, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 6b640bfe79b..0b3c113edb1 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.3.46", + "version": "7.3.54", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -924,6 +924,12 @@ "default": false, "description": "Enable chat textarea autocomplete" }, + "kilo-code.new.indexing.showButtonWhenDisabled": { + "type": "boolean", + "default": true, + "scope": "application", + "description": "Show the codebase indexing button below the prompt while indexing is disabled." + }, "kilo-code.new.claudeCodeCompat": { "type": "boolean", "default": false, @@ -1100,6 +1106,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", diff --git a/packages/kilo-vscode/script/build.ts b/packages/kilo-vscode/script/build.ts index 45257a191c5..74abe402ab8 100644 --- a/packages/kilo-vscode/script/build.ts +++ b/packages/kilo-vscode/script/build.ts @@ -2,7 +2,7 @@ import { $ } from "bun" import { join } from "node:path" import { existsSync, mkdirSync, rmSync, chmodSync } from "node:fs" -import { copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" +import { copySandboxResources, copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" import { ensureFfmpegForTarget } from "./ffmpeg-helper" const packageJsonPath = join(import.meta.dir, "..", "package.json") @@ -69,20 +69,15 @@ for (const config of targets) { const sourceBinary = join(cliDistDir, config.cliDir, "bin", config.binary) const targetBinary = join(binDir, config.binary) - const sourceSnapshot = join(cliDistDir, config.cliDir, "bin", "models-snapshot.json") - const targetSnapshot = join(binDir, "models-snapshot.json") if (!existsSync(sourceBinary)) { throw new Error(`CLI binary not found at ${sourceBinary}`) } - if (!existsSync(sourceSnapshot)) { - throw new Error(`CLI models snapshot not found at ${sourceSnapshot}`) - } console.log(` 📥 Copying binary from ${config.cliDir}/bin/${config.binary}...`) await $`cp ${sourceBinary} ${targetBinary}` - await $`cp ${sourceSnapshot} ${targetSnapshot}` await copyTreeSitterResources(sourceBinary, targetBinary) + await copySandboxResources(sourceBinary, targetBinary) if (config.binary !== "kilo.exe") { chmodSync(targetBinary, 0o755) diff --git a/packages/kilo-vscode/script/bwrap-helper.ts b/packages/kilo-vscode/script/bwrap-helper.ts new file mode 100644 index 00000000000..a66cc838c12 --- /dev/null +++ b/packages/kilo-vscode/script/bwrap-helper.ts @@ -0,0 +1,101 @@ +import { randomUUID } from "node:crypto" +import { + chmodSync, + constants, + copyFileSync, + lstatSync, + mkdirSync, + realpathSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs" +import { dirname } from "node:path" +import { localBwrapDigest, localBwrapPath, validLocalBwrap } from "../src/services/cli-backend/cli-resources" + +export function currentBwrapTarget(): string { + const os = process.platform === "win32" ? "win32" : process.platform + return `${os}-${process.arch}` +} + +function arch(target: string): "x64" | "arm64" { + if (target === "linux-x64") return "x64" + if (target === "linux-arm64") return "arm64" + throw new Error(`No Bubblewrap helper configured for target ${target}`) +} + +function source() { + const configured = process.env.KILO_BWRAP_PATH + if (configured) return realpathSync(configured) + + const found = Bun.which("bwrap") + if (!found) return + const target = realpathSync(found) + const entry = statSync(target) + const uid = process.getuid?.() + const groups = process.getgroups?.() ?? [] + const writable = + (entry.mode & 0o002) !== 0 || + (uid !== undefined && entry.uid === uid && (entry.mode & 0o200) !== 0) || + (groups.includes(entry.gid) && (entry.mode & 0o020) !== 0) + if (writable) { + throw new Error(`Refusing writable Bubblewrap executable at ${target}; set KILO_BWRAP_PATH to trust it explicitly`) + } + return target +} + +function secure(dir: string) { + mkdirSync(dir, { recursive: true, mode: 0o700 }) + const entry = lstatSync(dir) + if ( + entry.isSymbolicLink() || + !entry.isDirectory() || + entry.uid !== process.getuid?.() || + (entry.mode & 0o077) !== 0 + ) { + throw new Error(`Bubblewrap cache directory is not private: ${dir}`) + } +} + +function stage(source: string, dest: string, digest: string) { + const root = dirname(dirname(dest)) + const dir = dirname(dest) + secure(root) + secure(dir) + + const token = `${process.pid}-${randomUUID()}` + const executable = `${dest}.${token}.tmp` + const checksum = `${executable}.sha256` + try { + copyFileSync(source, executable, constants.COPYFILE_EXCL) + chmodSync(executable, 0o755) + writeFileSync(checksum, `${digest}\n`, { flag: "wx", mode: 0o600 }) + renameSync(executable, dest) + renameSync(checksum, `${dest}.sha256`) + } finally { + rmSync(executable, { force: true }) + rmSync(checksum, { force: true }) + } + + if (!validLocalBwrap(dest)) throw new Error(`Could not validate staged Bubblewrap executable at ${dest}`) +} + +export async function ensureBwrapForTarget(target: string, root?: string): Promise { + const dest = localBwrapPath(target, root) + if (!dest) return + + const executable = source() + if (executable) { + const digest = localBwrapDigest(executable) + if (validLocalBwrap(dest) && localBwrapDigest(dest) === digest) return dest + stage(executable, dest, digest) + return dest + } + if (validLocalBwrap(dest)) return dest + + const { buildBubblewrap } = await import("../../opencode/script/kilocode/bubblewrap") + const built = await buildBubblewrap(arch(target)) + stage(built.executable, dest, built.digest) + return dest +} diff --git a/packages/kilo-vscode/script/local-bin.ts b/packages/kilo-vscode/script/local-bin.ts index 2fd0a765db9..177a2d40778 100644 --- a/packages/kilo-vscode/script/local-bin.ts +++ b/packages/kilo-vscode/script/local-bin.ts @@ -2,7 +2,16 @@ import { $ } from "bun" import { join, relative, dirname, basename } from "node:path" import { chmodSync, statSync, rmSync, readdirSync, existsSync } from "node:fs" -import { copyTreeSitterResources, hasTreeSitterResources } from "../src/services/cli-backend/cli-resources" +import { + copyKiloSandboxWorker, + copySandboxResources, + copyTreeSitterResources, + hasKiloSandboxWorker, + hasTreeSitterResources, + kiloSandboxWorkerForBinary, + sanitizeSandboxResources, +} from "../src/services/cli-backend/cli-resources" +import { currentBwrapTarget, ensureBwrapForTarget } from "./bwrap-helper" import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper" const forceRebuild = process.argv.includes("--force") @@ -25,14 +34,12 @@ const opencodeDir = join(packagesDir, "opencode") const coreDir = join(packagesDir, "core") const gatewayDir = join(packagesDir, "kilo-gateway") const indexingDir = join(packagesDir, "kilo-indexing") +const sandboxDir = join(packagesDir, "kilo-sandbox") const targetBinDir = join(kiloVscodeDir, "bin") const binName = process.platform === "win32" ? "kilo.exe" : "kilo" const targetBinPath = join(targetBinDir, binName) -const snapshotName = "models-snapshot.json" -const targetSnapshotPath = join(targetBinDir, snapshotName) const versionFile = join(targetBinDir, ".cli-version") -const devSnapshotPath = join(opencodeDir, "src", "provider", snapshotName) function log(msg: string) { console.log(`[local-bin] ${msg}`) @@ -44,10 +51,8 @@ async function cliSourceHash(): Promise { const coreResult = await $`git log -1 --format=%H -- .`.cwd(coreDir).quiet() const gatewayResult = await $`git log -1 --format=%H -- .`.cwd(gatewayDir).quiet() const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet() - return ( - `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}` || - null - ) + const sandboxResult = await $`git log -1 --format=%H -- .`.cwd(sandboxDir).quiet() + return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}-${sandboxResult.text().trim()}` } catch { return null } @@ -59,11 +64,13 @@ async function isDirty(): Promise { const coreResult = await $`git status --porcelain -- .`.cwd(coreDir).quiet() const gatewayResult = await $`git status --porcelain -- .`.cwd(gatewayDir).quiet() const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet() + const sandboxResult = await $`git status --porcelain -- .`.cwd(sandboxDir).quiet() return ( opencodeResult.text().trim().length > 0 || coreResult.text().trim().length > 0 || gatewayResult.text().trim().length > 0 || - indexingResult.text().trim().length > 0 + indexingResult.text().trim().length > 0 || + sandboxResult.text().trim().length > 0 ) } catch { return false @@ -101,8 +108,7 @@ async function findKiloBinaryInOpencodeDist(): Promise { const preferred = join(distDir, `@kilocode`, tag, "bin", binName) try { statSync(preferred) - if (!hasTreeSitterResources(preferred)) return null - if (!existsSync(snapshotForBinary(preferred))) return null + if (!hasTreeSitterResources(preferred) || !hasKiloSandboxWorker(preferred)) return null return preferred } catch { // fall through to generic search @@ -128,8 +134,7 @@ async function findKiloBinaryInOpencodeDist(): Promise { continue } if (e.isFile() && (e.name === "kilo" || e.name === "kilo.exe") && basename(dirname(p)) === "bin") { - if (!hasTreeSitterResources(p)) continue - if (!existsSync(snapshotForBinary(p))) continue + if (!hasTreeSitterResources(p) || !hasKiloSandboxWorker(p)) continue return p } } @@ -137,10 +142,6 @@ async function findKiloBinaryInOpencodeDist(): Promise { return null } -function snapshotForBinary(file: string): string { - return join(dirname(file), snapshotName) -} - async function ensureBuiltBinary(): Promise { const found = await findKiloBinaryInOpencodeDist() if (found) return found @@ -173,6 +174,24 @@ async function ensureBuiltBinary(): Promise { return built } +async function bundleKiloSandboxWorker() { + const result = await Bun.build({ + entrypoints: [join(sandboxDir, "src", "kilo-sandbox-mutation-worker.ts")], + target: "bun", + format: "esm", + minify: true, + }) + if (!result.success || result.outputs.length !== 1) throw new Error("Could not bundle Kilo sandbox mutation worker") + await Bun.write(kiloSandboxWorkerForBinary(targetBinPath), result.outputs[0]) +} + +async function ensureLocalHelpers() { + await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) + if (process.env.KILO_SKIP_BUNDLED_BWRAP === "1") return + if (await sanitizeSandboxResources(targetBinDir, true)) return + await ensureBwrapForTarget(currentBwrapTarget()) +} + async function writeSourceWrapper() { if (process.platform === "win32") { throw new Error("Compiled CLI build failed and source wrapper fallback is not supported on Windows.") @@ -191,8 +210,8 @@ async function writeSourceWrapper() { ].join("\n"), ) chmodSync(targetBinPath, 0o755) - if (existsSync(devSnapshotPath)) await $`cp ${devSnapshotPath} ${targetSnapshotPath}` - await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) + await bundleKiloSandboxWorker() + await ensureLocalHelpers() const hash = await cliSourceHash() if (hash) await Bun.write(versionFile, hash + "\n") @@ -204,18 +223,17 @@ async function writeSourceWrapper() { async function main() { const targetFile = Bun.file(targetBinPath) const exists = await targetFile.exists() - const snapshotExists = await Bun.file(targetSnapshotPath).exists() - const ready = exists && snapshotExists + const ready = exists && hasTreeSitterResources(targetBinPath) && hasKiloSandboxWorker(targetBinPath) const stale = ready && !forceRebuild && (await isStale()) - const rebuild = forceRebuild || stale || (exists && !snapshotExists) + const rebuild = forceRebuild || stale || !ready if (ready && !rebuild) { const st = statSync(targetBinPath) log( `CLI binary already present at ${relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB). Use --force to rebuild.`, ) - await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) + await ensureLocalHelpers() return } @@ -226,8 +244,7 @@ async function main() { if (exists && rebuild) { log(stale ? `CLI source has changed — rebuilding.` : `Refreshing existing CLI resources.`) rmSync(targetBinPath) - if (existsSync(targetSnapshotPath)) rmSync(targetSnapshotPath) - if (forceRebuild || stale) { + if (forceRebuild || stale || !ready) { removeDist() } } @@ -243,15 +260,14 @@ async function main() { return null }) if (!sourceBinPath) return - const sourceSnapshotPath = snapshotForBinary(sourceBinPath) await $`mkdir -p ${targetBinDir}` - await $`cp ${sourceSnapshotPath} ${targetSnapshotPath}` await $`cp ${sourceBinPath} ${targetBinPath}` await copyTreeSitterResources(sourceBinPath, targetBinPath) + await copySandboxResources(sourceBinPath, targetBinPath) + await copyKiloSandboxWorker(sourceBinPath, targetBinPath) chmodSync(targetBinPath, 0o755) - await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) + await ensureLocalHelpers() - // Record the CLI source version so future runs detect when a rebuild is needed const hash = await cliSourceHash() if (hash) await Bun.write(versionFile, hash + "\n") diff --git a/packages/kilo-vscode/script/watch-cli.ts b/packages/kilo-vscode/script/watch-cli.ts index 2d0ff2b371c..e7326362843 100644 --- a/packages/kilo-vscode/script/watch-cli.ts +++ b/packages/kilo-vscode/script/watch-cli.ts @@ -7,9 +7,9 @@ * CLI backend without manual rebuild steps. */ import { watch, chmodSync } from "node:fs" -import { dirname, join, relative } from "node:path" +import { join, relative } from "node:path" import { $ } from "bun" -import { copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" +import { copySandboxResources, copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" const kiloVscodeDir = join(import.meta.dir, "..") const packagesDir = join(kiloVscodeDir, "..") @@ -17,7 +17,6 @@ const opencodeDir = join(packagesDir, "opencode") const opencodeSrcDir = join(opencodeDir, "src") const targetBinDir = join(kiloVscodeDir, "bin") const targetBinPath = join(targetBinDir, "kilo") -const snapshotName = "models-snapshot.json" let building = false let pending = false @@ -31,10 +30,6 @@ function sourceBinaryPath(): string { return join(opencodeDir, "dist", `@kilocode/cli-${process.platform}-${process.arch}`, "bin", "kilo") } -function snapshotPath(binary: string): string { - return join(dirname(binary), snapshotName) -} - async function rebuild() { if (building) { pending = true @@ -56,20 +51,15 @@ async function rebuild() { installed = true const source = sourceBinaryPath() - const snapshot = snapshotPath(source) if (!(await Bun.file(source).exists())) { log(`ERROR: Build completed but no binary found at ${relative(packagesDir, source)}`) return } - if (!(await Bun.file(snapshot).exists())) { - log(`ERROR: Build completed but no models snapshot found at ${relative(packagesDir, snapshot)}`) - return - } await $`mkdir -p ${targetBinDir}` - await $`cp ${snapshot} ${join(targetBinDir, snapshotName)}` await $`cp ${source} ${targetBinPath}` await copyTreeSitterResources(source, targetBinPath) + await copySandboxResources(source, targetBinPath) chmodSync(targetBinPath, 0o755) const elapsed = ((performance.now() - start) / 1000).toFixed(1) @@ -93,9 +83,8 @@ let timer: ReturnType | null = null watch(opencodeSrcDir, { recursive: true }, (_event, filename) => { if (!filename) return - // Skip non-source files and build-generated files + // Skip test files if (filename.endsWith(".test.ts") || filename.endsWith(".test.tsx")) return - if (filename.includes("models-snapshot")) return if (timer) clearTimeout(timer) timer = setTimeout(() => { diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 3ef1f944182..118ae7243ec 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -15,6 +15,7 @@ import { previewSound } from "./services/attention" import type { EditorContext, IndexingStatus } from "./services/cli-backend/types" import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete" +import { notebookUri } from "./services/autocomplete/continuedev/core/autocomplete/notebook" import { buildWebviewHtml, getWebviewFontSize } from "./utils" import { saveImage } from "./kilo-provider/save-image" import { handleEditorAction } from "./kilo-provider/editor-actions" @@ -72,7 +73,7 @@ import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-pa import { childID } from "./kilo-provider/task-session" import { VisibleTaskStreams } from "./kilo-provider/visible-task-streams" import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network" -import { abortSession } from "./kilo-provider/abort" +import { SessionAbort } from "./kilo-provider/abort" import { buildAutocompleteSettingsMessage, validAutocompleteSetting, @@ -94,8 +95,8 @@ import { hasGit } from "./kilo-provider/git-status" // legacy-migration start import { checkAndShowMigrationWizard, - handleRequestLegacyMigrationData, - handleStartLegacyMigration, + handleRequestMigrationData, + handleStartMigration, handleFinalizeLegacyMigration, handleSkipLegacyMigration, handleClearLegacyData, @@ -150,6 +151,11 @@ import { createAutoApproveBridge } from "./kilo-provider/auto-approve" import type { KiloProviderOptions } from "./kilo-provider/options" import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway" import { stopSessionProcesses } from "./kilo-provider/background-process" +import { + buildIndexingSettingsMessage, + validIndexingSetting, + watchIndexingConfig, +} from "./kilo-provider/indexing-settings" type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile" type ContextMessage = { contextDirectory?: unknown } @@ -172,6 +178,16 @@ const SESSION_SCOPED_PART_EVENTS = new Set(["message.part.updated", "message.par const isSessionScopedPartEvent = (type: string) => SESSION_SCOPED_PART_EVENTS.has(type) type SyncPayload = Extract +type RawSyncPayload = { + type: "sync" + syncEvent: { + type: SyncPayload["name"] + id: string + seq: number + aggregateID: string + data: unknown + } +} type LegacySyncEvent = | { id: string @@ -201,6 +217,7 @@ type LegacySyncEvent = | { source: "sync" id: string + seq: number type: "session.updated" properties: Extract["data"] } @@ -234,24 +251,35 @@ function isFullSessionUpdatedEvent(event: ProviderEvent): event is FullSessionUp return event.type === "session.updated" && !isLegacySyncEvent(event) } -function unwrapSyncEvent(event: GlobalEvent["payload"]): ProviderEvent | undefined { +export function unwrapSyncEvent(event: GlobalEvent["payload"] | RawSyncPayload): ProviderEvent | undefined { if (event.type !== "sync") return event + const payload = + "syncEvent" in event + ? ({ + type: "sync", + name: event.syncEvent.type, + id: event.syncEvent.id, + seq: event.syncEvent.seq, + aggregateID: event.syncEvent.aggregateID, + data: event.syncEvent.data, + } as SyncPayload) + : event - switch (event.name) { + switch (payload.name) { case "message.updated.1": - return { id: event.id, type: "message.updated", properties: event.data } + return { id: payload.id, type: "message.updated", properties: payload.data } case "message.removed.1": - return { id: event.id, type: "message.removed", properties: event.data } + return { id: payload.id, type: "message.removed", properties: payload.data } case "message.part.updated.1": - return { id: event.id, type: "message.part.updated", properties: event.data } + return { id: payload.id, type: "message.part.updated", properties: payload.data } case "message.part.removed.1": - return { id: event.id, type: "message.part.removed", properties: event.data } + return { id: payload.id, type: "message.part.removed", properties: payload.data } case "session.created.1": - return { id: event.id, type: "session.created", properties: event.data } + return { id: payload.id, type: "session.created", properties: payload.data } case "session.updated.1": - return { source: "sync", id: event.id, type: "session.updated", properties: event.data } + return { source: "sync", id: payload.id, seq: payload.seq, type: "session.updated", properties: payload.data } case "session.deleted.1": - return { id: event.id, type: "session.deleted", properties: event.data } + return { id: payload.id, type: "session.deleted", properties: payload.data } default: return undefined } @@ -266,6 +294,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** Remembers the last selected session so /new can stay in the same worktree after clearSession. */ private contextSessionID: string | undefined private connectionState: "connecting" | "connected" | "disconnected" | "error" = "connecting" + private connectionGeneration = 0 private loginAttempt = 0 private isWebviewReady = false private readonly extensionVersion = @@ -282,6 +311,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private providersRefresh: Promise | null = null private providersQueued = false private providersGeneration = 0 + private sandboxRevision = 0 private cachedAgentsMessage: unknown = null /** Cached skillsLoaded payload so requestSkills can be served before client is ready */ private cachedSkillsMessage: unknown = null @@ -308,8 +338,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private promptRecovery: Promise | null = null private trackedSessionIds: Set = new Set() private syncedChildSessions: Set = new Set() + private readonly checkpoints = new Map>() + private readonly sessionCreations = new Map>() + private readonly sandboxTransitions = new Map>() + private readonly revisions = new Map() + private readonly refreshes = new Map() private sessionStatusMap = new Map() // Latest status used for destructive config warnings. private sessionDirectories = new Map() // Per-session directory overrides, such as Agent Manager worktrees. + private readonly aborts = new SessionAbort() private projectID: string | undefined // Current workspace project ID used to filter sessions. private loadMessagesAbort: AbortController | null = null // Current load request cancellation. private lastReconciledAt = new Map() // Per-session focus-mode reconcile timestamp. @@ -319,8 +355,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private readonly confirmations = new MessageConfirmation() private unsubscribeEvent: (() => void) | null = null private unsubscribeState: (() => void) | null = null - /** Cached legacy migration data so migrate() doesn't re-read from disk/SecretStorage. */ // legacy-migration - private cachedLegacyData: import("./legacy-migration/legacy-types").LegacyMigrationData | null = null // legacy-migration + /** Cached migration data so migration doesn't re-read from disk/SecretStorage. */ // legacy-migration + private migrationCache: MigrationContext["migrationCache"] = new Map() /** Guard to prevent checkAndShowMigrationWizard running concurrently. */ // legacy-migration private migrationCheckInFlight = false // legacy-migration private unsubscribeNotificationDismiss: (() => void) | null = null @@ -333,6 +369,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private initConnectionPromise: Promise | null = null private webviewMessageDisposable: vscode.Disposable | null = null private autocompleteConfigDisposable: vscode.Disposable | null = null + private indexingConfigDisposable: vscode.Disposable | null = null private telemetryStateDisposable: vscode.Disposable | null = null private viewStateDisposable: vscode.Disposable | null = null private visibilityDisposable: vscode.Disposable | null = null @@ -388,10 +425,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } private setCurrentSession(session: Session | null): void { + const ids = new Set([this.currentSession?.id, session?.id]) + for (const id of ids) { + if (id) this.refreshes.set(id, (this.refreshes.get(id) ?? 0) + 1) + } this.currentSession = session this.opts.tabTitle?.(nativeTitle(session)) } + private checkpoint(sid: string, run: () => Promise): void { + const prior = this.checkpoints.get(sid) ?? Promise.resolve() + const pending = prior.catch(() => undefined).then(run) + const cleanup = () => { + if (this.checkpoints.get(sid) === pending) this.checkpoints.delete(sid) + } + this.checkpoints.set(sid, pending) + void pending.then(cleanup, (error) => { + console.error("[Kilo New] checkpoint mutation failed:", error) + cleanup() + }) + } + private stopCurrentSessionProcesses(next?: string): void { const sid = this.contextSessionID ?? this.currentSession?.id if (!sid || sid === next) return @@ -545,6 +599,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper data: profileData, }) + if (this.currentSession) { + this.refreshSessionDetails(this.currentSession.id, this.getWorkspaceDirectory(this.currentSession.id)) + } + // Re-send cached worktree stats and git status after webview reload. if (this.cachedStats) this.postMessage(this.cachedStats) this.postMessage({ type: "gitStatus", repo: this.cachedGitRepo }) @@ -657,11 +715,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper * When set, all operations for this session use this directory instead of the workspace root. */ public setSessionDirectory(sessionId: string, directory: string): void { + this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), this.getWorkspaceDirectory(sessionId)) this.sessionDirectories.set(sessionId, directory) + if (this.connectionState === "connected") void this.fetchAndSendSandboxStatus(sessionId) } public clearSessionDirectory(sessionId: string): void { + this.aborts.preserve(sessionId, this.sessionStatusMap.get(sessionId), this.getWorkspaceDirectory(sessionId)) this.sessionDirectories.delete(sessionId) + if (this.connectionState === "connected") void this.fetchAndSendSandboxStatus(sessionId) } /** Exposes the session→directory map so callers outside the webview can resolve worktree paths. */ @@ -747,6 +809,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.webviewMessageDisposable?.dispose() this.autocompleteConfigDisposable?.dispose() this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg)) + this.indexingConfigDisposable?.dispose() + this.indexingConfigDisposable = watchIndexingConfig((msg) => this.postMessage(msg)) this.telemetryStateDisposable?.dispose() this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { @@ -850,14 +914,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.handleAbort(message.sessionID) break case "revertSession": - this.handleRevertSession(message.sessionID, message.messageID, message.partID).catch((e) => - console.error("[Kilo New] handleRevertSession failed:", e), + this.checkpoint(message.sessionID, () => + this.handleRevertSession(message.sessionID, message.messageID, message.partID), ) break case "unrevertSession": - this.handleUnrevertSession(message.sessionID).catch((e) => - console.error("[Kilo New] handleUnrevertSession failed:", e), - ) + this.checkpoint(message.sessionID, () => this.handleUnrevertSession(message.sessionID)) break case "permissionResponse": await handlePermissionResponse( @@ -1022,6 +1084,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.pendingFollowup = null await handleQuestionReject(this.questionCtx, message.requestID, message.sessionID) break + case "requestSandboxStatus": + await this.fetchAndSendSandboxStatus(message.sessionID) + break + case "toggleSandbox": + await this.handleToggleSandbox(message) + break case "requestConfig": this.fetchAndSendConfig().catch((e) => console.error("[Kilo New] fetchAndSendConfig failed:", e)) break @@ -1033,6 +1101,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.error("[Kilo New] fetchAndSendIndexingStatus failed:", e), ) break + case "requestIndexingSettings": + this.postMessage(buildIndexingSettingsMessage()) + break case "requestKiloEmbeddingModels": this.fetchAndSendKiloEmbeddingModels().catch((e) => console.error("[Kilo New] fetchAndSendKiloEmbeddingModels failed:", e), @@ -1203,11 +1274,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper break } // legacy-migration start - case "requestLegacyMigrationData": - void handleRequestLegacyMigrationData(this.migrationCtx) + case "requestMigrationData": + void handleRequestMigrationData(this.migrationCtx, message.source, message.operationId) break - case "startLegacyMigration": - void handleStartLegacyMigration(this.migrationCtx, message.selections) + case "startMigration": + void handleStartMigration(this.migrationCtx, message.source, message.operationId, message.selections) break case "skipLegacyMigration": void handleSkipLegacyMigration(this.migrationCtx) @@ -1279,6 +1350,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.log("[Kilo New] KiloProvider: 🔧 Starting initializeConnection...") this.connectionState = "connecting" + this.connectionGeneration++ this.postMessage({ type: "connectionState", state: "connecting" }) // Clean up any existing subscriptions (e.g., sidebar re-shown) @@ -1331,6 +1403,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Subscribe to connection state changes this.unsubscribeState = this.connectionService.onStateChange(async (state, error) => { + if (this.connectionState !== state) this.connectionGeneration++ this.connectionState = state this.postConnectionState(error) @@ -1382,7 +1455,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // legacy-migration start // Subscribe to migration-complete broadcast from any KiloProvider instance this.unsubscribeMigrationComplete = this.connectionService.onMigrationComplete(() => { - this.postMessage({ type: "migrationState", needed: false }) + this.postMessage({ type: "migrationState", needed: false, source: "legacy" }) }) // legacy-migration end @@ -1503,13 +1576,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** Non-blocking: refresh session metadata + status for the webview after switching. */ private refreshSessionDetails(sessionID: string, dir: string, signal?: AbortSignal): void { if (!this.client) return + const revision = this.revisions.get(sessionID) + const refresh = (this.refreshes.get(sessionID) ?? 0) + 1 + this.refreshes.set(sessionID, refresh) this.client.session .get({ sessionID, directory: dir }) .then((r) => { - if (r.data && !signal?.aborted && this.contextSessionID === sessionID) { - this.setCurrentSession(r.data) - this.contextSessionID = r.data.id + if (!r.data || signal?.aborted || this.contextSessionID !== sessionID) return + if (this.refreshes.get(sessionID) !== refresh) { + if (this.revisions.get(sessionID) !== revision) this.refreshSessionDetails(sessionID, dir, signal) + return } + if (this.revisions.get(sessionID) !== revision) { + this.refreshSessionDetails(sessionID, dir, signal) + return + } + this.setCurrentSession(r.data) + this.contextSessionID = r.data.id + this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(r.data) }) }) .catch((e: unknown) => console.warn("[Kilo New] KiloProvider: getSession failed (non-critical):", e)) this.postMessage({ type: "workspaceDirectoryChanged", directory: this.getWorkspaceDirectory(sessionID) }) @@ -1755,7 +1839,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.visibleTaskStreams.delete(sessionID) this.syncedChildSessions.delete(sessionID) this.sessionDirectories.delete(sessionID) + this.aborts.delete(sessionID) this.lastReconciledAt.delete(sessionID) + this.checkpoints.delete(sessionID) + this.revisions.delete(sessionID) + this.refreshes.delete(sessionID) this.connectionService.pruneSession(sessionID) if (this.currentSession?.id === sessionID) { this.contextSessionID = undefined @@ -2382,6 +2470,143 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage(getWorkStylePayload()) } + private postSandboxError(sessionID: string, error: unknown, revision: number, requestID?: string): void { + this.postMessage({ + type: "sandboxStatusError", + sessionID, + directory: this.getWorkspaceDirectory(sessionID), + message: getErrorMessage(error) || "Failed to update sandbox", + requestID, + revision, + }) + } + + private async fetchAndSendSandboxStatus(sessionID: string, requestID?: string): Promise { + const revision = ++this.sandboxRevision + const generation = this.connectionGeneration + const client = this.client + const sandbox = client?.sandbox + if (!sandbox?.status) return + if (this.connectionState !== "connected") { + this.postSandboxError(sessionID, "Not connected to CLI backend", revision, requestID) + return + } + try { + const directory = this.getWorkspaceDirectory(sessionID) + const { data } = await sandbox.status({ sessionID, directory }, { throwOnError: true }) + if (this.connectionState !== "connected" || this.connectionGeneration !== generation || this.client !== client) + return + if (!sameDirectory(data.directory, this.getWorkspaceDirectory(sessionID))) { + if (requestID) void this.fetchAndSendSandboxStatus(sessionID, requestID) + return + } + this.postMessage({ type: "sandboxStatus", sessionID, revision, ...data, requestID }) + } catch (error) { + if (this.connectionState !== "connected" || this.connectionGeneration !== generation || this.client !== client) + return + this.postSandboxError(sessionID, error, revision, requestID) + } + } + + private sandboxKey(input: { + sessionID?: string + draftID?: string + agentManagerContext?: string + contextDirectory?: string + }): string { + if (input.sessionID) return `session:${input.sessionID}` + if (input.draftID) return `draft:${input.draftID}` + return `context:${input.agentManagerContext ?? ""}:${input.contextDirectory ?? this.getRootDirectory()}` + } + + private handleToggleSandbox(input: { + sessionID?: string + draftID?: string + requestID: string + agentManagerContext?: string + contextDirectory?: string + }): Promise { + const key = this.sandboxKey(input) + const pending = this.sandboxTransitions.get(key) + if (pending) return pending.catch(() => undefined) + const operation = this.runToggleSandbox(input, key) + this.sandboxTransitions.set(key, operation) + return operation + .catch(() => undefined) + .finally(() => { + for (const [id, active] of this.sandboxTransitions) { + if (active === operation) this.sandboxTransitions.delete(id) + } + }) + } + + private async runToggleSandbox( + input: { + sessionID?: string + draftID?: string + requestID: string + agentManagerContext?: string + contextDirectory?: string + }, + key: string, + ): Promise { + const revision = ++this.sandboxRevision + const generation = this.connectionGeneration + const client = this.client + const sandbox = client?.sandbox + if (!sandbox?.toggle || this.connectionState !== "connected") { + const error = new Error("Not connected to CLI backend") + this.postSandboxError(input.sessionID ?? "", error, revision, input.requestID) + throw error + } + const resolved = await this.resolveSession( + input.sessionID, + input.draftID, + input.agentManagerContext, + input.contextDirectory, + ).catch((error) => { + this.postSandboxError(input.sessionID ?? "", error, revision, input.requestID) + throw error + }) + if (!resolved) { + const error = new Error("Failed to resolve sandbox session") + this.postSandboxError(input.sessionID ?? "", error, revision, input.requestID) + throw error + } + const operation = this.sandboxTransitions.get(key) + if (operation) this.sandboxTransitions.set(`session:${resolved.sid}`, operation) + if (this.connectionGeneration !== generation || this.client !== client) { + throw new Error("Sandbox connection changed") + } + try { + const { data } = await sandbox.toggle( + { sessionID: resolved.sid, directory: resolved.dir }, + { throwOnError: true }, + ) + if (this.connectionState !== "connected" || this.connectionGeneration !== generation || this.client !== client) { + throw new Error("Sandbox connection changed") + } + if (!data.available) throw new Error(data.reason ?? "Sandbox backend is unavailable") + if (!sameDirectory(data.directory, this.getWorkspaceDirectory(resolved.sid))) { + throw new Error("Session directory changed during sandbox toggle") + } + this.postMessage({ + type: "sandboxStatus", + sessionID: resolved.sid, + revision, + ...data, + requestID: input.requestID, + }) + vscode.window.showInformationMessage(data.enabled ? "Sandbox enabled" : "Sandbox disabled") + } catch (error) { + if (this.connectionState === "connected" && this.connectionGeneration === generation && this.client === client) { + this.postSandboxError(resolved.sid, error, revision, input.requestID) + void this.fetchAndSendSandboxStatus(resolved.sid) + } + throw error + } + } + private async handleUpdateConfig( partial: Partial, project: Partial = {}, @@ -2496,21 +2721,29 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) if (!sessionID && !this.currentSession) { - const { data: session } = await this.client.session.create( - { directory: dir, platform: this.opts.platform }, - { throwOnError: true }, - ) - this.stopCurrentSessionProcesses(session.id) - this.setCurrentSession(session) - this.contextSessionID = session.id - this.focusSession(session.id) - this.trackDirectory(session.id, dir) - this.trackedSessionIds.add(session.id) - this.postMessage({ - type: "sessionCreated", - session: this.sessionToWebview(session), - draftID, - }) + const key = `${draftID ?? context ?? "new"}\0${dir}` + const pending = this.sessionCreations.get(key) + if (pending) return pending + const creation = (async () => { + const { data: session } = await this.client!.session.create( + { directory: dir, platform: this.opts.platform }, + { throwOnError: true }, + ) + this.stopCurrentSessionProcesses(session.id) + this.setCurrentSession(session) + this.contextSessionID = session.id + this.focusSession(session.id) + this.trackDirectory(session.id, dir) + this.trackedSessionIds.add(session.id) + this.postMessage({ + type: "sessionCreated", + session: this.sessionToWebview(session), + draftID, + }) + return { sid: session.id, dir } + })().finally(() => this.sessionCreations.delete(key)) + this.sessionCreations.set(key, creation) + return creation } const sid = sessionID || this.currentSession?.id @@ -2624,7 +2857,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper let resolved: { sid: string; dir: string } | undefined try { + const sandbox = this.sandboxTransitions.get( + this.sandboxKey({ sessionID, draftID, agentManagerContext: context, contextDirectory }), + ) resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory) + if (sandbox) await sandbox const parts: Array = [] if (files) { @@ -2642,6 +2879,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.connectionService.recordMessageSessionId(messageID, sid) } + await this.checkpoints.get(sid) await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Message request", () => this.withRetry( () => @@ -2704,7 +2942,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper let resolved: { sid: string; dir: string } | undefined try { + const sandbox = this.sandboxTransitions.get( + this.sandboxKey({ sessionID, draftID, agentManagerContext: context, contextDirectory }), + ) resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory) + if (sandbox) await sandbox if (messageID) { this.connectionService.recordMessageSessionId(messageID, resolved!.sid) @@ -2720,6 +2962,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const sid = resolved!.sid const dir = resolved!.dir + await this.checkpoints.get(sid) await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Command request", () => this.withRetry( () => @@ -2754,24 +2997,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } private async handleAbort(sessionID?: string): Promise { - if (!this.client) { - return - } - - const targetSessionID = sessionID || this.currentSession?.id - if (!targetSessionID) { - return - } - - try { - await abortSession({ - client: this.client, - sessionID: targetSessionID, - dir: this.getWorkspaceDirectory(targetSessionID), - }) - } catch (error) { - console.error("[Kilo New] KiloProvider: Failed to abort session:", error) - } + const sid = sessionID || this.currentSession?.id + if (!this.client || !sid || !(await this.aborts.stop(this.client, sid, this.getWorkspaceDirectory(sid)))) return + this.sessionStatusMap.set(sid, "idle") + this.streams.flush(sid) + this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" }) } private async handleRevertSession(sessionID: string, messageID: string, partID?: string): Promise { @@ -2781,9 +3011,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (error) { console.error("[Kilo New] KiloProvider: Failed to revert session:", error) this.postMessage({ type: "error", message: "Failed to revert session", sessionID }) - return + throw error } - if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) + if (!data) throw new Error("Revert returned no session") + this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1) + if (this.currentSession?.id === sessionID) this.setCurrentSession(data) + this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) } private async handleUnrevertSession(sessionID: string): Promise { @@ -2793,9 +3026,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (error) { console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error) this.postMessage({ type: "error", message: "Failed to redo session", sessionID }) - return + throw error } - if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) + if (!data) throw new Error("Redo returned no session") + this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1) + if (this.currentSession?.id === sessionID) this.setCurrentSession(data) + this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) }) } /** @@ -2941,6 +3177,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private async handleUpdateSetting(key: string, value: unknown): Promise { const { section, leaf } = buildSettingPath(key) if (section === "autocomplete" && !validAutocompleteSetting(leaf, value)) return + if (section === "indexing" && !validIndexingSetting(leaf, value)) return const config = vscode.workspace.getConfiguration(`kilo-code.new${section ? `.${section}` : ""}`) // Normalize a webview-side clear to `undefined` so VS Code removes the // key from settings.json rather than persisting a literal `null`. This @@ -2987,6 +3224,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Re-send all settings to the webview so the UI reflects the reset this.postMessage(buildAutocompleteSettingsMessage()) + this.postMessage(buildIndexingSettingsMessage()) this.sendBrowserSettings() this.sendNotificationSettings() this.sendTimelineSetting() @@ -3156,6 +3394,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "session.status") { const sid = event.properties.sessionID this.sessionStatusMap.set(sid, event.properties.status.type) + this.aborts.observe(sid, event.properties.status.type, directory) const msg = mapSSEEventToWebviewMessage(event, sid) if (msg) { this.streams.flush(sid) @@ -3179,6 +3418,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return } + if (event.type === "session.updated") { + // Full bus snapshots duplicate sync patches with the same event ID but no sequence metadata. + if (isFullSessionUpdatedEvent(event)) return + const sid = event.properties.sessionID + const revision = this.revisions.get(sid) + const versioned = event.seq > 0 || (revision?.seq ?? 0) > 0 + if (revision && (versioned ? event.seq <= revision.seq : event.id <= revision.id)) return + this.revisions.set(sid, { id: event.id, seq: event.seq }) + } + // Refresh provider and agent lists when the server signals a state disposal if (event.type === "global.disposed") { void this.reloadAfterAuthChange() @@ -3188,6 +3437,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "server.instance.disposed") { const props = event.properties as Record | null const dir = typeof props?.directory === "string" ? props.directory : undefined + if (dir) for (const sid of this.aborts.dispose(dir)) this.sessionStatusMap.set(sid, "idle") if (dir && !sameDirectory(dir, this.getWorkspaceDirectory())) return void this.reloadAfterAuthChange() return @@ -3209,10 +3459,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.trackedSessionIds.add(event.properties.info.id) } if (event.type === "session.updated" && this.currentSession?.id === event.properties.sessionID) { - const session = isLegacySyncEvent(event) - ? applySessionPatch(this.currentSession, event.properties.info) - : event.properties.info - this.setCurrentSession(session) + this.setCurrentSession(applySessionPatch(this.currentSession, event.properties.info)) this.contextSessionID = event.properties.sessionID } @@ -3255,15 +3502,18 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const msg = isLegacySyncEvent(event) ? this.mapSyncEventToWebviewMessage(event) - : isFullSessionUpdatedEvent(event) - ? { type: "sessionUpdated" as const, session: this.sessionToWebview(event.properties.info) } - : mapSSEEventToWebviewMessage(event, sessionID) + : mapSSEEventToWebviewMessage(event, sessionID) if (!msg) return if (msg.type === "partUpdated") { this.streams.push({ ...msg, part: this.slimPart(msg.part) }) return } const next = msg.type === "messageCreated" ? { ...msg, message: this.slimInfo(msg.message) } : msg + if (next.type === "sandboxStatus") { + if (!sameDirectory(next.directory, this.getWorkspaceDirectory(next.sessionID))) return + this.postMessage({ ...next, revision: ++this.sandboxRevision }) + return + } if (next.type === "indexingStatusLoaded") { this.cachedIndexingStatusMessage = next } @@ -3355,14 +3605,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const result = new Set() for (const group of vscode.window.tabGroups.all) { for (const tab of group.tabs) { - if (tab.input instanceof vscode.TabInputText) { - const uri = tab.input.uri - if (uri.scheme === "file") { - const rel = path.relative(dir, uri.fsPath) - if (!rel.startsWith("..") && !path.isAbsolute(rel) && controller.validateAccess(uri.fsPath)) { - result.add(rel.replaceAll("\\", "/")) - } - } + const uri = + tab.input instanceof vscode.TabInputText || tab.input instanceof vscode.TabInputNotebook + ? tab.input.uri + : undefined + if (uri?.scheme !== "file") continue + + const rel = path.relative(dir, uri.fsPath) + if (!rel.startsWith("..") && !path.isAbsolute(rel) && controller.validateAccess(uri.fsPath)) { + result.add(rel.replaceAll("\\", "/")) } } } @@ -3400,21 +3651,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } // Visible files (capped to avoid bloating context, filtered through .kilocodeignore) - const visibleFiles = vscode.window.visibleTextEditors - .map((e) => e.document.uri) - .filter((uri) => uri.scheme === "file") - .map((uri) => toRelative(uri.fsPath)) - .filter((p): p is string => p !== undefined && controller.validateAccess(path.resolve(workspaceDir, p))) - .slice(0, 200) + const visibleFiles = [ + ...new Set( + [ + ...vscode.window.visibleTextEditors.map((editor) => notebookUri(editor.document.uri)), + ...vscode.window.visibleNotebookEditors.map((editor) => editor.notebook.uri), + ] + .filter((uri): uri is vscode.Uri => uri?.scheme === "file") + .map((uri) => toRelative(uri.fsPath)) + .filter( + (file): file is string => file !== undefined && controller.validateAccess(path.resolve(workspaceDir, file)), + ), + ), + ].slice(0, 200) - // Open tabs — use instanceof TabInputText to exclude notebooks, diffs, custom editors + // Open tabs — text and notebook files only; exclude diffs and custom editors const openTabs = [...(await this.getOpenTabPaths(workspaceDir))].slice(0, 20) // Active file (also filtered through .kilocodeignore) const activeEditor = vscode.window.activeTextEditor - const activeRel = - activeEditor?.document.uri.scheme === "file" ? toRelative(activeEditor.document.uri.fsPath) : undefined - const activeFile = activeRel && controller.validateAccess(activeEditor!.document.uri.fsPath) ? activeRel : undefined + const activeUri = activeEditor + ? notebookUri(activeEditor.document.uri) + : vscode.window.activeNotebookEditor?.notebook.uri + const activeRel = activeUri ? toRelative(activeUri.fsPath) : undefined + const activeFile = activeRel && activeUri && controller.validateAccess(activeUri.fsPath) ? activeRel : undefined // Shell const shell = vscode.env.shell || undefined @@ -3519,12 +3779,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper client: this.client, extensionContext: this.extensionContext, postMessage: (msg) => this.postMessage(msg), - get cachedLegacyData() { - return self.cachedLegacyData - }, - set cachedLegacyData(data) { - self.cachedLegacyData = data - }, + migrationCache: self.migrationCache, get migrationCheckInFlight() { return self.migrationCheckInFlight }, @@ -3590,6 +3845,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.visibilityDisposable?.dispose() this.webviewMessageDisposable?.dispose() this.autocompleteConfigDisposable?.dispose() + this.indexingConfigDisposable?.dispose() this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() this.visibleTaskStreams.clear() @@ -3600,6 +3856,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.trackedSessionIds.clear() this.syncedChildSessions.clear() this.sessionDirectories.clear() + this.aborts.clear() this.sessionStatusMap.clear() this.ignoreController?.dispose() this.chatAutocomplete?.dispose() diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 81dbc54dabf..3e2b59e592f 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -439,7 +439,7 @@ export class AgentManagerProvider implements Disposable { return null } - if ((m.type === "sendMessage" || m.type === "sendCommand") && !m.sessionID) { + if ((m.type === "sendMessage" || m.type === "sendCommand" || m.type === "toggleSandbox") && !m.sessionID) { const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined if (worktree) { @@ -448,7 +448,11 @@ export class AgentManagerProvider implements Disposable { } } - if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) { + if ( + (m.type === "sendMessage" || m.type === "sendCommand" || m.type === "toggleSandbox") && + m.draftID && + !m.sessionID + ) { this.activeSessionId = m.draftID return msg } diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 189045d5a76..dbe7aa4867f 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -203,8 +203,10 @@ export class SessionTerminalManager { return result } + const disposable = this.tryRegisterCommand(id, handler) + if (!disposable) return this.commandHandlers.set(id, handler) - this.commandDisposables.set(id, this.host.registerCommand(id, handler)) + this.commandDisposables.set(id, disposable) } private async runOriginalCommand(id: string, args: unknown[]): Promise { @@ -218,9 +220,20 @@ export class SessionTerminalManager { return await this.host.executeCommand(id, ...args) } finally { const handler = this.commandHandlers.get(id) - if (!handler) return - const replacement = this.host.registerCommand(id, handler) - this.commandDisposables.set(id, replacement) + if (handler) { + const replacement = this.tryRegisterCommand(id, handler) + if (replacement) this.commandDisposables.set(id, replacement) + } + } + } + + private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise): Disposable | undefined { + try { + return this.host.registerCommand(id, handler) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + this.log(`panel command registration skipped for ${id}: ${msg}`) + return undefined } } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index ed766104333..e48b9fa7a4f 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -639,6 +639,15 @@ interface SendCommandIn { contextDirectory?: string } +interface ToggleSandboxIn { + type: "toggleSandbox" + sessionID?: string + draftID?: string + requestID: string + agentManagerContext?: string + contextDirectory?: string +} + interface RequestTerminalContextIn { type: "requestTerminalContext" requestId: string @@ -783,6 +792,7 @@ export type AgentManagerInMessage = | LoadMessagesIn | SendMessageIn | SendCommandIn + | ToggleSandboxIn | RequestTerminalContextIn | ClearSessionIn | AbortIn diff --git a/packages/kilo-vscode/src/commands/toggle-auto-approve.ts b/packages/kilo-vscode/src/commands/toggle-auto-approve.ts index 21eca5f8ce2..a94873fc219 100644 --- a/packages/kilo-vscode/src/commands/toggle-auto-approve.ts +++ b/packages/kilo-vscode/src/commands/toggle-auto-approve.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import type { KiloClient } from "@kilocode/sdk/v2/client" +import type { Event, KiloClient } from "@kilocode/sdk/v2/client" import type { KiloConnectionService } from "../services/cli-backend/connection-service" /** @@ -13,9 +13,11 @@ export type DirectoryResolver = (sessionId?: string) => string * (workspace root + all registered worktree paths). */ export type AllDirectories = () => string[] +type Asked = Extract export interface AutoApproveController { active(): boolean + approve(event: Asked, directory?: string): Promise toggle(): Promise onChange(listener: (active: boolean) => void): { dispose(): void } } @@ -26,9 +28,9 @@ const KEY = "enabled" /** * Runtime auto-accept toggle for permissions. * - * Instead of writing to the config file, we intercept `permission.asked` SSE - * events and auto-reply "once" to each. This avoids config-layer issues - * (merged vs global, sparse defaults) and works even when the sidebar is closed. + * Instead of writing to the CLI config, the attention coordinator delegates + * `permission.asked` events here and auto-replies "once". This avoids config-layer + * issues (merged vs global, sparse defaults) and works even when the sidebar is closed. */ export function registerToggleAutoApprove( context: vscode.ExtensionContext, @@ -71,9 +73,11 @@ export function registerToggleAutoApprove( const { data: pending } = await client.permission.list({ directory: dir }, { throwOnError: true }) for (const req of pending) { if (generation !== snapshot) break - await client.permission.reply({ requestID: req.id, directory: dir, reply: "once" }).catch((err) => { - console.error("[Kilo New] toggleAutoApprove: failed to drain pending:", err) - }) + await client.permission + .reply({ requestID: req.id, directory: dir, reply: "once" }, { throwOnError: true }) + .catch((err) => { + console.error("[Kilo New] toggleAutoApprove: failed to drain pending:", err) + }) } } catch (err) { console.error("[Kilo New] toggleAutoApprove: failed to list pending permissions:", err) @@ -83,19 +87,23 @@ export function registerToggleAutoApprove( return active } - const unsubscribe = connectionService.onEvent((event, directory) => { - if (!active) return - if (event.type !== "permission.asked") return + const approve = async (event: Asked, directory?: string) => { + if (!active) return false const client = tryGetClient(connectionService) - if (!client) return + if (!client) return false const dir = directory ?? connectionService.getPermissionDirectory(event.properties.id) ?? resolve(event.properties.sessionID) - client.permission.reply({ requestID: event.properties.id, directory: dir, reply: "once" }).catch((err) => { - console.error("[Kilo New] toggleAutoApprove: failed to auto-reply:", err) - }) - }) + return client.permission + .reply({ requestID: event.properties.id, directory: dir, reply: "once" }, { throwOnError: true }) + .then( + () => true, + (err) => { + console.error("[Kilo New] toggleAutoApprove: failed to auto-reply:", err) + return false + }, + ) + } - context.subscriptions.push({ dispose: unsubscribe }) context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((event) => { if (!event.affectsConfiguration(`${CONFIG}.${KEY}`)) return @@ -111,6 +119,7 @@ export function registerToggleAutoApprove( return { active: () => active, + approve, toggle, onChange(listener) { listeners.add(listener) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index a760f2ce211..55cd1480d91 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -49,7 +49,6 @@ export function activate(context: vscode.ExtensionContext) { // Create shared connection service (one server for all webviews) const connectionService = new KiloConnectionService(context) - const attention = new AttentionService(connectionService) let restore = context.workspaceState.get(RESTORE_KEY) ?? {} const remember = (patch: RestoreState) => { const next = { ...restore, ...patch } @@ -103,9 +102,6 @@ export function activate(context: vscode.ExtensionContext) { }), ) - // Prewarm the CLI backend early so autocomplete is ready before first editor use. - ensureBackendForAutocomplete(connectionService) - for (const folder of vscode.workspace.workspaceFolders ?? []) { void markWorkspace(folder.uri.fsPath, (msg) => console.warn(`[Kilo New] ${msg}`)) } @@ -181,6 +177,13 @@ export function activate(context: vscode.ExtensionContext) { return [...dirs] }, ) + const attention = new AttentionService(connectionService, { + approve: (event, directory) => autoApprove.approve(event, directory), + }) + + // Prewarm only after all global event consumers are ready. + ensureBackendForAutocomplete(connectionService) + provider.setAutoApproveController(autoApprove) agentManagerHost.setAutoApproveController(autoApprove) @@ -387,7 +390,7 @@ export function activate(context: vscode.ExtensionContext) { }), // legacy-migration start vscode.commands.registerCommand("kilo-code.new.openMigrationWizard", () => { - provider.postMessage({ type: "migrationState", needed: true }) + provider.postMessage({ type: "migrationState", needed: true, source: "legacy" }) }), // legacy-migration end vscode.commands.registerCommand("kilo-code.new.generateTerminalCommand", async () => { diff --git a/packages/kilo-vscode/src/features.ts b/packages/kilo-vscode/src/features.ts index 0e0423b9046..618eaa79bf1 100644 --- a/packages/kilo-vscode/src/features.ts +++ b/packages/kilo-vscode/src/features.ts @@ -1,4 +1,5 @@ import { hasIndexingPlugin } from "@kilocode/kilo-indexing/detect" +import * as vscode from "vscode" type PluginSpec = string | [string, Record] @@ -8,10 +9,12 @@ type ConfigLike = { export type Features = { indexing: boolean + sandboxControls: boolean } export function configFeatures(config?: ConfigLike | null): Features { return { indexing: hasIndexingPlugin(config?.plugin ?? []), + sandboxControls: vscode.workspace.getConfiguration("kilo-code.new.internal").get("sandboxControls", false), } } diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index b862887edf6..ac41b677480 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -518,6 +518,15 @@ export type WebviewMessage = | { type: "sessionDeleted"; sessionID: string } | { type: "messageRemoved"; sessionID: string; messageID: string } | { type: "sessionError"; sessionID?: string; error?: unknown } + | { + type: "sandboxStatus" + sessionID: string + directory: string + enabled: boolean + available: boolean + reason?: string + version: number + } | null type PartEvent = @@ -554,6 +563,12 @@ function mapPartEvent(event: PartEvent, sessionID: string | undefined): WebviewM } } +function statusExtra(info: Extract["properties"]["status"]) { + if (info.type === "retry") return { attempt: info.attempt, message: info.message, next: info.next } + if (info.type === "offline") return { message: info.message } + return {} +} + export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: string | undefined): WebviewMessage { if (event.type === "sync") { switch (event.name) { @@ -595,12 +610,7 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin case "session.status": { const info = event.properties.status const status = info.type - const extra = - info.type === "retry" - ? { attempt: info.attempt, message: info.message, next: info.next } - : info.type === "offline" - ? { message: info.message } - : {} + const extra = statusExtra(info) return { type: "sessionStatus" as const, sessionID: event.properties.sessionID, @@ -681,6 +691,16 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin error: event.properties.error, } } + case "sandbox.status.changed": + return { + type: "sandboxStatus", + sessionID: event.properties.sessionID, + directory: event.properties.directory, + enabled: event.properties.enabled, + available: event.properties.available, + reason: event.properties.reason, + version: event.properties.version, + } case "indexing.status": return { type: "indexingStatusLoaded", diff --git a/packages/kilo-vscode/src/kilo-provider/abort.ts b/packages/kilo-vscode/src/kilo-provider/abort.ts index 8d3d00676e9..7bb355b5b26 100644 --- a/packages/kilo-vscode/src/kilo-provider/abort.ts +++ b/packages/kilo-vscode/src/kilo-provider/abort.ts @@ -1,4 +1,69 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" +import type { KiloClient, SessionStatus } from "@kilocode/sdk/v2/client" +import { sameDirectory } from "../kilo-provider-utils" + +export class SessionAbort { + private active = new Map>() + + observe(sessionID: string, status: SessionStatus["type"], dir?: string) { + if (!dir) return + const dirs = this.active.get(sessionID) + if (status === "idle") { + if (!dirs) return + for (const entry of dirs) { + if (sameDirectory(entry, dir)) dirs.delete(entry) + } + if (dirs.size === 0) this.active.delete(sessionID) + return + } + if (!dirs) { + this.active.set(sessionID, new Set([dir])) + return + } + if (![...dirs].some((entry) => sameDirectory(entry, dir))) dirs.add(dir) + } + + preserve(sessionID: string, status: SessionStatus["type"] | undefined, dir: string) { + if (!status || status === "idle" || this.active.has(sessionID)) return + this.observe(sessionID, status, dir) + } + + async stop(client: KiloClient, sessionID: string, fallback: string) { + const known = this.active.has(sessionID) + const dirs = [...(this.active.get(sessionID) ?? [])] + if (!dirs.some((dir) => sameDirectory(dir, fallback))) dirs.push(fallback) + const results = await Promise.allSettled(dirs.map((dir) => abortSession({ client, sessionID, dir }))) + const failures = results.flatMap((result, index) => + result.status === "rejected" ? [{ dir: dirs[index], error: result.reason }] : [], + ) + if (failures.length > 0) { + console.error("[Kilo New] KiloProvider: Failed to abort session in one or more directories:", failures) + return false + } + if (known) this.active.delete(sessionID) + return known + } + + dispose(dir: string) { + const idle: string[] = [] + for (const [sessionID, dirs] of this.active) { + for (const entry of dirs) { + if (sameDirectory(entry, dir)) dirs.delete(entry) + } + if (dirs.size > 0) continue + this.active.delete(sessionID) + idle.push(sessionID) + } + return idle + } + + delete(sessionID: string) { + this.active.delete(sessionID) + } + + clear() { + this.active.clear() + } +} export async function abortSession(input: { client: KiloClient; sessionID: string; dir: string }) { await input.client.session.abort({ sessionID: input.sessionID, directory: input.dir }, { throwOnError: true }) diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts index a0a06c210c0..70c0f9c76cd 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts @@ -2,7 +2,7 @@ * Legacy migration handlers — extracted from KiloProvider. * * Manages the migration wizard for users upgrading from Kilo Code v5.x. - * No vscode dependency — all vscode access is injected via MigrationContext. + * VS Code access is limited to migration service helpers and injected context. */ import type { KiloClient } from "@kilocode/sdk/v2/client" @@ -10,8 +10,13 @@ import type { LegacyMigrationData, MigrationSelections, MigrationSessionProgress, + MigrationSessionSelection, } from "../../legacy-migration/legacy-types" import * as MigrationService from "../../legacy-migration/migration-service" +import { runSessionBatch } from "../../legacy-migration/session-batch" +import { migrate as migrateSession } from "../../legacy-migration/sessions/migrate" +import { resolveSession } from "../../legacy-migration/task-store" +import { detectRooCodeSessions, type RooImportSource } from "../../roo-import/service" /** Subset of vscode.ExtensionContext needed by migration handlers. */ interface MigrationExtensionContext { @@ -28,21 +33,59 @@ interface MigrationExtensionContext { globalStorageUri: { fsPath: string } } +export type MigrationSource = "legacy" | "roo" +export type MigrationCacheEntry = + | { operationId: string; source: "legacy"; data: LegacyMigrationData } + | { operationId: string; source: "roo"; data: RooImportSource | null } +export type MigrationCache = Map + +export function getMigrationCache( + cache: MigrationCache, + source: "legacy", + operationId: string, +): Extract | undefined +export function getMigrationCache( + cache: MigrationCache, + source: "roo", + operationId: string, +): Extract | undefined +export function getMigrationCache(cache: MigrationCache, source: MigrationSource, operationId: string) { + const entry = cache.get(operationId) + return entry?.source === source ? entry : undefined +} + export interface MigrationContext { readonly client: KiloClient | null readonly extensionContext: MigrationExtensionContext | undefined postMessage(msg: unknown): void refreshSessions(): void - cachedLegacyData: LegacyMigrationData | null + migrationCache: MigrationCache migrationCheckInFlight: boolean lastMigrationHadErrors?: boolean disposeGlobal(): Promise broadcastComplete(): void } -function postSessionProgress(ctx: MigrationContext, progress: MigrationSessionProgress): void { +function emptyData(sessions: LegacyMigrationData["sessions"] = []): LegacyMigrationData { + return { + hasData: sessions.length > 0, + providers: [], + mcpServers: [], + customModes: [], + sessions, + } +} + +function postSessionProgress( + ctx: MigrationContext, + source: MigrationSource, + operationId: string, + progress: MigrationSessionProgress, +): void { ctx.postMessage({ - type: "legacyMigrationSessionProgress", + type: "migrationSessionProgress", + source, + operationId, session: progress.session, index: progress.index, total: progress.total, @@ -75,13 +118,42 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis if (!data.hasData) return - // Cache so migrate() doesn't re-read from SecretStorage/disk - ctx.cachedLegacyData = data - console.log("[Kilo New] KiloProvider: 🔄 Legacy data detected, showing migration wizard") + // The wizard re-requests the data via requestMigrationData on mount, so only the flag is sent here. ctx.postMessage({ type: "migrationState", needed: true, + source: "legacy", + }) +} + +/** Send migration data for the requested source to the webview. */ +export async function handleRequestMigrationData( + ctx: MigrationContext, + source: MigrationSource, + operationId: string, +): Promise { + if (!ctx.extensionContext) return + // A new request means a new wizard session; drop any entry from an abandoned one. + for (const key of ctx.migrationCache.keys()) { + if (key !== operationId) ctx.migrationCache.delete(key) + } + const data = await (async () => { + if (source === "roo") { + const roo = await detectRooCodeSessions(ctx.extensionContext as Parameters[0]) + ctx.migrationCache.set(operationId, { operationId, source, data: roo }) + return emptyData(roo?.sessions ?? []) + } + const legacy = await MigrationService.detectLegacyData( + ctx.extensionContext as Parameters[0], + ) + ctx.migrationCache.set(operationId, { operationId, source, data: legacy }) + return legacy + })() + ctx.postMessage({ + type: "migrationData", + source, + operationId, data: { providers: data.providers, mcpServers: data.mcpServers, @@ -93,54 +165,83 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis }) } -/** Send the detected legacy data to the webview on explicit request. */ -export async function handleRequestLegacyMigrationData(ctx: MigrationContext): Promise { - if (!ctx.extensionContext) return - const data = await MigrationService.detectLegacyData( - ctx.extensionContext as Parameters[0], - ) - ctx.cachedLegacyData = data - ctx.postMessage({ - type: "legacyMigrationData", - data: { - providers: data.providers, - mcpServers: data.mcpServers, - customModes: data.customModes, - sessions: data.sessions, - defaultModel: data.defaultModel, - settings: data.settings, +async function startRooMigration( + ctx: MigrationContext, + operationId: string, + selections: { sessions?: MigrationSessionSelection[] }, +): Promise { + if (!ctx.extensionContext || !ctx.client) return + const cached = getMigrationCache(ctx.migrationCache, "roo", operationId) + const source = cached + ? cached.data + : await detectRooCodeSessions(ctx.extensionContext as Parameters[0]) + if (!cached) ctx.migrationCache.set(operationId, { operationId, source: "roo", data: source }) + if (!source) { + ctx.postMessage({ + type: "migrationComplete", + source: "roo", + operationId, + results: [ + { item: "Roo Code sessions", category: "session", status: "warning", message: "No Roo Code sessions found." }, + ], + }) + return + } + + const results = await runSessionBatch({ + selections: selections.sessions ?? [], + sessions: source.sessions, + resolve: (id) => resolveSession(source.catalog, id), + migrate: (selection, resolved, progress) => + migrateSession( + selection, + ctx.extensionContext as Parameters[1], + ctx.client as KiloClient, + progress, + resolved, + ), + onProgress: (item, status, message) => { + ctx.postMessage({ type: "migrationProgress", source: "roo", operationId, item, status, message }) }, + onSessionProgress: (progress) => postSessionProgress(ctx, "roo", operationId, progress), }) + + ctx.lastMigrationHadErrors = results.some((item) => item.status === "error") + ctx.postMessage({ type: "migrationComplete", source: "roo", operationId, results }) } /** Run the migration for the selected items. */ -export async function handleStartLegacyMigration( +async function startLegacyMigration( ctx: MigrationContext, + operationId: string, selections: MigrationSelections, ): Promise { if (!ctx.extensionContext || !ctx.client) return try { + const cached = getMigrationCache(ctx.migrationCache, "legacy", operationId) const results = await MigrationService.migrate( ctx.extensionContext as Parameters[0], ctx.client, selections, (item, status, message) => { - ctx.postMessage({ type: "legacyMigrationProgress", item, status, message }) + ctx.postMessage({ type: "migrationProgress", source: "legacy", operationId, item, status, message }) }, (progress: MigrationSessionProgress) => { - postSessionProgress(ctx, progress) + postSessionProgress(ctx, "legacy", operationId, progress) }, - ctx.cachedLegacyData?.settings, - ctx.cachedLegacyData?.sessions, + cached?.data.settings, + cached?.data.sessions, ) ctx.lastMigrationHadErrors = results.some((item) => item.status === "error") - ctx.postMessage({ type: "legacyMigrationComplete", results }) + ctx.postMessage({ type: "migrationComplete", source: "legacy", operationId, results }) } catch (error) { ctx.lastMigrationHadErrors = true console.error("[Kilo New] KiloProvider: ❌ Migration failed", error) ctx.postMessage({ - type: "legacyMigrationComplete", + type: "migrationComplete", + source: "legacy", + operationId, results: [ { item: "Migration", @@ -153,6 +254,24 @@ export async function handleStartLegacyMigration( } } +export async function handleStartMigration( + ctx: MigrationContext, + source: MigrationSource, + operationId: string, + selections: MigrationSelections, +): Promise { + try { + if (source === "roo") { + await startRooMigration(ctx, operationId, selections) + return + } + await startLegacyMigration(ctx, operationId, selections) + } finally { + // The operation has finished (or thrown); its cached discovery is no longer needed. + ctx.migrationCache.delete(operationId) + } +} + export async function handleFinalizeLegacyMigration(ctx: MigrationContext): Promise { if (!ctx.extensionContext) return await ctx.disposeGlobal() diff --git a/packages/kilo-vscode/src/kilo-provider/indexing-settings.ts b/packages/kilo-vscode/src/kilo-provider/indexing-settings.ts new file mode 100644 index 00000000000..8fbd0ace48e --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/indexing-settings.ts @@ -0,0 +1,25 @@ +import * as vscode from "vscode" + +type Post = (msg: unknown) => void + +export function buildIndexingSettingsMessage() { + const config = vscode.workspace.getConfiguration("kilo-code.new.indexing") + return { + type: "indexingSettingsLoaded" as const, + settings: { + showButtonWhenDisabled: config.get("showButtonWhenDisabled", true), + }, + } +} + +export function watchIndexingConfig(post: Post): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("kilo-code.new.indexing")) { + post(buildIndexingSettingsMessage()) + } + }) +} + +export function validIndexingSetting(key: string, value: unknown) { + return key === "showButtonWhenDisabled" && typeof value === "boolean" +} diff --git a/packages/kilo-vscode/src/legacy-migration/migration-service.ts b/packages/kilo-vscode/src/legacy-migration/migration-service.ts index 7a2bb2036b0..9ac01ab8f4c 100644 --- a/packages/kilo-vscode/src/legacy-migration/migration-service.ts +++ b/packages/kilo-vscode/src/legacy-migration/migration-service.ts @@ -36,9 +36,11 @@ import type { MigrationSessionInfo, MigrationSessionProgress, } from "./legacy-types" -import { buildSessionMeta, buildSessionProgress } from "./migration-session-progress" import type { MigrationResultItem } from "./migration-types" +import { runSessionBatch } from "./session-batch" +import { listSessions, resolveSession, scanTaskStore } from "./task-store" import { createSessionID } from "./sessions/lib/ids" +import type { LegacyHistoryItem } from "./sessions/lib/legacy-types" import { migrate as migrateSession } from "./sessions/migrate" // --------------------------------------------------------------------------- @@ -77,7 +79,7 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis const customModes = await readLegacyCustomModes(context) const prompts = readLegacyCustomModePrompts(context) const settings = readLegacySettings(context) - const sessions = await readSessionsInGlobalStorage(context) + const sessions = listSessions(await readSessionCatalog(context)) const oauthProviders = new Set() const codexRaw = await context.secrets.get(CODEX_OAUTH_SECRET_KEY) @@ -116,28 +118,10 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis } } -async function readSessionsInGlobalStorage(context: vscode.ExtensionContext) { - const items = context.globalState.get<{ id: string; task?: string; workspace?: string; ts?: number }[]>( - "taskHistory", - [], - ) - const base = vscode.Uri.joinPath(context.globalStorageUri, "tasks") - const sessions: MigrationSessionInfo[] = [] - for (const item of items) { - const file = vscode.Uri.joinPath(base, item.id, "api_conversation_history.json") - const exists = await vscode.workspace.fs.stat(file).then( - () => true, - () => false, - ) - if (!exists) continue - sessions.push({ - id: item.id, - title: item.task?.trim() || item.id, - directory: item.workspace?.trim() || "", - time: item.ts ?? 0, - }) - } - return sessions +async function readSessionCatalog(context: vscode.ExtensionContext) { + const items = context.globalState.get("taskHistory", []) + const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath + return (await scanTaskStore(dir, items, { mode: "history" })).catalog } // --------------------------------------------------------------------------- @@ -152,9 +136,6 @@ export type ProgressCallback = ( export type SessionProgressCallback = (progress: MigrationSessionProgress) => void -const SESSION_DELAY = 300 -const SESSION_SUMMARY_DELAY = 1000 - /** * Executes migration for the selected items. * Calls onProgress for each item with real-time status updates. @@ -176,7 +157,8 @@ export async function migrate( const customModes = await readLegacyCustomModes(context) const prompts = readLegacyCustomModePrompts(context) const legacySettings = cachedSettings ?? readLegacySettings(context) - const sessions = cachedSessions ?? (await readSessionsInGlobalStorage(context)) + const catalog = await readSessionCatalog(context) + const sessions = cachedSessions ?? listSessions(catalog) const results: MigrationResultItem[] = [] @@ -277,36 +259,16 @@ export async function migrate( } if (selections.sessions?.length) { - const list = selections.sessions - for (const [index, item] of list.entries()) { - onProgress(item.id, "migrating") - const session = sessions.find((entry: MigrationSessionInfo) => entry.id === item.id) - const meta = buildSessionMeta(session, index, list.length) - const progress = buildSessionProgress(meta, onSessionProgress) - const result = await migrateSession(item, context, client, meta, progress) - const reason = result.ok ? "Session migrated" : result.message - results.push({ - item: item.id, - category: "session", - status: result.ok ? "success" : "error", - message: reason, - }) - onProgress(item.id, result.ok ? "success" : "error", reason) - if (index < list.length - 1) { - await new Promise((resolve) => setTimeout(resolve, SESSION_DELAY)) - } - } - const last = list.at(-1) - const session = last ? sessions.find((item: MigrationSessionInfo) => item.id === last.id) : undefined - if (session && onSessionProgress) { - onSessionProgress({ - session, - index: list.length, - total: list.length, - phase: "summary", - }) - await new Promise((resolve) => setTimeout(resolve, SESSION_SUMMARY_DELAY)) - } + results.push( + ...(await runSessionBatch({ + selections: selections.sessions, + sessions, + resolve: (id) => resolveSession(catalog, id), + migrate: (selection, source, progress) => migrateSession(selection, context, client, progress, source), + onProgress, + onSessionProgress, + })), + ) } // Migrate default model diff --git a/packages/kilo-vscode/src/legacy-migration/session-batch.ts b/packages/kilo-vscode/src/legacy-migration/session-batch.ts new file mode 100644 index 00000000000..4fb009b4037 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/session-batch.ts @@ -0,0 +1,63 @@ +import type { MigrationSessionInfo, MigrationSessionProgress, MigrationSessionSelection } from "./legacy-types" +import type { MigrationResultItem } from "./migration-types" +import { buildSessionMeta, buildSessionProgress } from "./migration-session-progress" +import type { SessionSource } from "./task-store" +import type { migrate as migrateSession } from "./sessions/migrate" + +const DELAY = 300 +const SUMMARY_DELAY = 1000 + +type Result = Awaited> + +interface BatchOptions { + selections: MigrationSessionSelection[] + sessions: MigrationSessionInfo[] + resolve(id: string): SessionSource | undefined + migrate( + selection: MigrationSessionSelection, + source: SessionSource, + progress: ReturnType, + ): Promise + onProgress(item: string, status: "migrating" | "success" | "warning" | "error", message?: string): void + onSessionProgress?: (progress: MigrationSessionProgress) => void + delay?: (ms: number) => Promise +} + +export async function runSessionBatch(options: BatchOptions): Promise { + const results: MigrationResultItem[] = [] + const wait = options.delay ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) + + for (const [index, selection] of options.selections.entries()) { + options.onProgress(selection.id, "migrating") + const session = options.sessions.find((entry) => entry.id === selection.id) + const source = options.resolve(selection.id) + if (!source) { + const message = "Session source not found" + results.push({ item: session?.title ?? selection.id, category: "session", status: "error", message }) + options.onProgress(selection.id, "error", message) + continue + } + + const meta = buildSessionMeta(session, index, options.selections.length) + const result = await options.migrate(selection, source, buildSessionProgress(meta, options.onSessionProgress)) + const status = result.ok ? (result.skipped ? "warning" : "success") : "error" + const message = result.ok ? (result.skipped ? "Already imported." : undefined) : result.message + results.push({ item: session?.title ?? selection.id, category: "session", status, message }) + options.onProgress(selection.id, status, message) + if (index < options.selections.length - 1) await wait(DELAY) + } + + const last = options.selections.at(-1) + const session = last ? options.sessions.find((entry) => entry.id === last.id) : undefined + if (session && options.onSessionProgress) { + options.onSessionProgress({ + session, + index: options.selections.length, + total: options.selections.length, + phase: "summary", + }) + await wait(SUMMARY_DELAY) + } + + return results +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts index 725aacf6059..123ae58cfff 100644 --- a/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/session.ts @@ -7,10 +7,11 @@ export function createSession( item: LegacyHistoryItem | undefined, projectID: string, dir: string, + key = id, ): NonNullable { const session = makeSession() - session.id = createSessionID(id) + session.id = createSessionID(key) session.projectID = projectID diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts index a77ca130fa2..1dfd2a6eb59 100644 --- a/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts +++ b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts @@ -1,8 +1,9 @@ import * as vscode from "vscode" import type { KiloClient } from "@kilocode/sdk/v2/client" import { getMigrationErrorMessage } from "../errors/migration-error" -import type { MigrationSessionInfo, MigrationSessionProgress, MigrationSessionSelection } from "../legacy-types" +import type { MigrationSessionProgress, MigrationSessionSelection } from "../legacy-types" import { createSessionID } from "./lib/ids" +import type { SessionSource } from "../task-store" import type { LegacyHistoryItem } from "./lib/legacy-types" import { parseSession } from "./parser" @@ -30,19 +31,19 @@ export async function migrate( input: MigrationSessionSelection, context: vscode.ExtensionContext, client: KiloClient, - meta?: { - session: MigrationSessionInfo - index: number - total: number - }, onProgress?: ProgressCallback, + resolved?: SessionSource, ): Promise { - const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath const items = context.globalState.get("taskHistory", []) - const item = items.find((item) => item.id === input.id) + const source = resolved ?? { + id: input.id, + dir: vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath, + item: items.find((item) => item.id === input.id), + } + const key = source.namespace ? `${source.namespace}:${source.id}` : source.id const progress = (next: Progress) => { - if (!meta || !onProgress) return + if (!onProgress) return onProgress(next) } @@ -68,12 +69,12 @@ export async function migrate( try { if (!input.force) { - const result = await client.session.get({ sessionID: createSessionID(input.id) }) + const result = await client.session.get({ sessionID: createSessionID(key) }) if (result.data) return skip() } progress({ phase: "preparing" }) - const payload = await parseSession(input.id, dir, item) + const payload = await parseSession(source.id, source.dir, source.item, undefined, key) progress({ phase: "storing" }) const project = await client.kilocode.sessionImport.project(payload.project, { throwOnError: true }) const projectID = project.data?.id ?? payload.project.id diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts b/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts index 966ba46bf41..57683c6d119 100644 --- a/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts +++ b/packages/kilo-vscode/src/legacy-migration/sessions/parser.ts @@ -1,4 +1,4 @@ -import type { LegacyHistoryItem } from "./lib/legacy-types" +import type { LegacyApiMessage, LegacyHistoryItem } from "./lib/legacy-types" import type { KilocodeSessionImportMessageData as Message, KilocodeSessionImportPartData as Part, @@ -19,15 +19,20 @@ export interface NormalizedSession { parts: Array> } -export async function parseSession(id: string, dir: string, item?: LegacyHistoryItem): Promise { +export async function parseSession( + id: string, + dir: string, + item?: LegacyHistoryItem, + input?: LegacyApiMessage[], + key = id, +): Promise { const root = await normalizeLegacyPath(item?.workspace) const next = item ? { ...item, workspace: root } : undefined const project = createProject(next) - const session = createSession(id, next, project.id, root) - const file = await getApiConversationHistory(id, dir) - const conversation = parseFile(file) - const messages = parseMessagesFromConversation(conversation, id, root, next) - const parts = parsePartsFromConversation(conversation, id, item) + const session = createSession(id, next, project.id, root, key) + const conversation = input ?? parseFile(await getApiConversationHistory(id, dir)) + const messages = parseMessagesFromConversation(conversation, key, root, next) + const parts = parsePartsFromConversation(conversation, key, item) return { project, diff --git a/packages/kilo-vscode/src/legacy-migration/task-store.ts b/packages/kilo-vscode/src/legacy-migration/task-store.ts new file mode 100644 index 00000000000..73daad0604f --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/task-store.ts @@ -0,0 +1,244 @@ +import * as path from "node:path" +import * as vscode from "vscode" +import type { MigrationSessionInfo } from "./legacy-types" +import type { LegacyHistoryItem } from "./sessions/lib/legacy-types" + +const API_FILE = "api_conversation_history.json" +const UI_FILE = "ui_messages.json" + +export interface SessionSource { + id: string + dir: string + item?: LegacyHistoryItem + namespace?: string + mtime?: number +} + +export interface SessionEntry { + id: string + session: MigrationSessionInfo + source: SessionSource +} + +export type SessionCatalog = Map + +export interface ScanDiagnostic { + id: string + dir: string + reason: "ui-only" | "malformed" | "missing-workspace" +} + +export interface TaskScan { + catalog: SessionCatalog + diagnostics: ScanDiagnostic[] +} + +export function listSessions(catalog: SessionCatalog) { + return [...catalog.values()].map((entry) => entry.session).sort((a, b) => b.time - a.time || a.id.localeCompare(b.id)) +} + +export function resolveSession(catalog: SessionCatalog, id: string) { + return catalog.get(id)?.source +} + +export type ScanMode = "history" | "discover" + +export interface ScanOptions { + namespace?: string + /** + * "history" trusts the provided history items and only checks that each task's + * conversation file still exists (cheap stat, used by legacy migration). + * "discover" enumerates every task directory on disk and parses conversation + * files to recover titles (used when no history is available, e.g. Roo import). + */ + mode?: ScanMode +} + +export async function scanTaskStore( + dir: string, + items: LegacyHistoryItem[] = [], + options: ScanOptions = {}, +): Promise { + const mode = options.mode ?? (items.length ? "history" : "discover") + return mode === "history" + ? scanFromHistory(dir, items, options.namespace) + : scanFromDisk(dir, items, options.namespace) +} + +/** Builds a catalog from known history items, only confirming each conversation file exists. */ +async function scanFromHistory(dir: string, items: LegacyHistoryItem[], namespace?: string): Promise { + const catalog: SessionCatalog = new Map() + + for (const item of items) { + if (catalog.has(item.id)) continue + if (!(await exists(path.join(dir, item.id, API_FILE)))) continue + catalog.set(item.id, { + id: item.id, + session: { + id: item.id, + title: item.task?.trim() || fallbackTitle(item.id), + directory: item.workspace?.trim() || "", + time: item.ts ?? timestamp(item.id), + }, + source: { id: item.id, dir, item, namespace }, + }) + } + + return { catalog, diagnostics: [] } +} + +/** Enumerates every task directory on disk and parses conversation files to recover titles. */ +async function scanFromDisk(dir: string, items: LegacyHistoryItem[], namespace?: string): Promise { + const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dir)).then( + (value) => value, + () => [] as [string, vscode.FileType][], + ) + const history = new Map(items.map((item) => [item.id, item])) + const catalog: SessionCatalog = new Map() + const diagnostics: ScanDiagnostic[] = [] + + for (const [id, type] of entries.sort(([a], [b]) => a.localeCompare(b))) { + if (type !== vscode.FileType.Directory) continue + const root = path.join(dir, id) + const api = path.join(root, API_FILE) + const valid = await readHistory(api) + if (!valid.exists) { + if (await exists(path.join(root, UI_FILE))) diagnostics.push({ id, dir, reason: "ui-only" }) + continue + } + if (!valid.valid) { + diagnostics.push({ id, dir, reason: "malformed" }) + continue + } + + const item = await readHistoryItem(root, id, valid.data, history.get(id)) + const workspace = item.workspace?.trim() + if (!workspace) { + diagnostics.push({ id, dir, reason: "missing-workspace" }) + continue + } + catalog.set(id, { + id, + session: { + id, + title: item.task?.trim() || fallbackTitle(id), + directory: workspace, + time: item.ts ?? timestamp(id), + }, + source: { id, dir, item, namespace, mtime: valid.mtime }, + }) + } + + return { catalog, diagnostics } +} + +export async function readTaskIndex(dir: string): Promise { + return Promise.resolve(vscode.workspace.fs.readFile(vscode.Uri.file(path.join(dir, "_index.json")))) + .then((bytes) => { + const json = JSON.parse(Buffer.from(bytes).toString("utf8")) as { entries?: unknown } + if (!Array.isArray(json.entries)) return [] + return json.entries.flatMap((value) => { + if (!value || typeof value !== "object") return [] + const item = value as Record + if (typeof item.id !== "string") return [] + return [parseRecord(item, item.id)] + }) + }) + .catch(() => []) +} + +async function readHistory(file: string) { + const uri = vscode.Uri.file(file) + return Promise.all([vscode.workspace.fs.readFile(uri), vscode.workspace.fs.stat(uri)]) + .then(([bytes, stat]) => { + const json = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown + return { + exists: true as const, + valid: Array.isArray(json), + data: Array.isArray(json) ? json : [], + mtime: stat.mtime, + } + }) + .catch((error) => { + if (error instanceof SyntaxError) { + return { exists: true as const, valid: false, data: [] as unknown[], mtime: 0 } + } + return { exists: false as const, valid: false, data: [] as unknown[], mtime: 0 } + }) +} + +async function readHistoryItem( + root: string, + id: string, + messages: unknown[], + indexed?: LegacyHistoryItem, +): Promise { + const file = path.join(root, "history_item.json") + const stored = await Promise.resolve(vscode.workspace.fs.readFile(vscode.Uri.file(file))) + .then((bytes) => parseItem(Buffer.from(bytes).toString("utf8"), id)) + .catch(() => undefined) + if (stored) return stored + if (indexed) return indexed + + return { + id, + task: titleFromMessages(messages) || fallbackTitle(id), + workspace: "", + ts: timestamp(id), + } +} + +function parseItem(input: string, id: string): LegacyHistoryItem | undefined { + return parseRecord(JSON.parse(input) as Record, id) +} + +function parseRecord(json: Record, id: string): LegacyHistoryItem { + return { + id, + task: typeof json.task === "string" ? json.task.trim().slice(0, 120) : fallbackTitle(id), + workspace: typeof json.workspace === "string" ? json.workspace : "", + ts: typeof json.ts === "number" ? json.ts : timestamp(id), + mode: typeof json.mode === "string" ? json.mode : undefined, + rootTaskId: typeof json.rootTaskId === "string" ? json.rootTaskId : undefined, + parentTaskId: typeof json.parentTaskId === "string" ? json.parentTaskId : undefined, + } +} + +function titleFromMessages(messages: unknown[]) { + for (const value of messages) { + if (!value || typeof value !== "object") continue + const msg = value as { role?: string; content?: unknown } + if (msg.role !== "user") continue + const text = textFromContent(msg.content) + if (text) return text.slice(0, 120).replace(/\n/g, " ").trim() + } + return "" +} + +function textFromContent(content: unknown): string { + if (typeof content === "string") return content + if (!Array.isArray(content)) return "" + for (const value of content) { + if (!value || typeof value !== "object") continue + const block = value as { type?: string; text?: unknown } + if (block.type === "text" && typeof block.text === "string" && block.text.trim()) return block.text + } + return "" +} + +function exists(file: string) { + return vscode.workspace.fs.stat(vscode.Uri.file(file)).then( + () => true, + () => false, + ) +} + +function timestamp(id: string) { + const value = Number(id) + return Number.isFinite(value) && value > 1_000_000_000_000 ? value : 0 +} + +function fallbackTitle(id: string) { + const time = timestamp(id) + return time ? new Date(time).toLocaleString() : id +} diff --git a/packages/kilo-vscode/src/roo-import/service.ts b/packages/kilo-vscode/src/roo-import/service.ts new file mode 100644 index 00000000000..1225fc7d540 --- /dev/null +++ b/packages/kilo-vscode/src/roo-import/service.ts @@ -0,0 +1,64 @@ +import * as path from "node:path" +import * as vscode from "vscode" +import { + listSessions, + readTaskIndex, + scanTaskStore, + type ScanDiagnostic, + type SessionCatalog, + type SessionEntry, +} from "../legacy-migration/task-store" + +const ROOTS = [ + "roovscode.roo-cline", + "roovscode.roo-code", + "rooveterinaryinc.roo-cline", + "rooveterinaryinc.roo-code", + "rooveterinaryinc.roo-code-nightly", +] + +export interface RooImportSource { + catalog: SessionCatalog + sessions: ReturnType + diagnostics: ScanDiagnostic[] +} + +/** Scans every known Roo storage root and keeps the most recent, complete copy of duplicate task IDs. */ +export async function detectRooCodeSessions( + context: vscode.ExtensionContext, + customPath?: string, +): Promise { + const parent = path.dirname(context.globalStorageUri.fsPath) + const configured = customPath ?? vscode.workspace.getConfiguration("roo-cline").get("customStoragePath", "") + const custom = typeof configured === "string" ? configured.trim() : "" + const roots = [ + ...ROOTS.map((root) => path.join(parent, root, "tasks")), + ...(custom ? [path.join(custom, "tasks")] : []), + ] + const dirs = [...new Map(roots.map((dir) => [path.resolve(dir), dir])).values()] + const catalog: SessionCatalog = new Map() + const diagnostics: ScanDiagnostic[] = [] + + for (const dir of dirs) { + const items = await readTaskIndex(dir) + const scan = await scanTaskStore(dir, items, { namespace: "roo", mode: "discover" }) + diagnostics.push(...scan.diagnostics) + for (const [id, entry] of [...scan.catalog].sort(([a], [b]) => a.localeCompare(b))) { + const current = catalog.get(id) + if (!current || compare(entry, current) >= 0) catalog.set(id, entry) + } + } + + for (const diagnostic of diagnostics) { + console.warn(`[Kilo New] Roo import skipped ${diagnostic.reason} task ${diagnostic.id} in ${diagnostic.dir}`) + } + + const sessions = listSessions(catalog) + return sessions.length ? { catalog, sessions, diagnostics } : null +} + +function compare(a: SessionEntry, b: SessionEntry) { + const complete = (entry: SessionEntry) => + Number(Boolean(entry.source.item?.task?.trim())) + Number(Boolean(entry.source.item?.mode)) + return (a.source.mtime ?? 0) - (b.source.mtime ?? 0) || a.session.time - b.session.time || complete(a) - complete(b) +} diff --git a/packages/kilo-vscode/src/services/attention/service.ts b/packages/kilo-vscode/src/services/attention/service.ts index c3659933ce3..e70c1310dcf 100644 --- a/packages/kilo-vscode/src/services/attention/service.ts +++ b/packages/kilo-vscode/src/services/attention/service.ts @@ -7,10 +7,15 @@ import { playSound, resolveSoundID } from "./sound" type Sync = Extract type Question = Extract type Permission = Extract +type Asked = Extract type Status = Extract type Close = Extract type Error = Extract +type Options = { + approve?: (event: Asked, directory?: string) => boolean | Promise +} + export function previewSound(value: string) { void playSound("default", resolveSoundID(value)) } @@ -20,12 +25,14 @@ export class AttentionService implements vscode.Disposable { private readonly errored = new Set() private readonly questions = new Set() private readonly permissions = new Set() - private readonly parents = new Map() private readonly unsubscribeEvent: () => void private readonly unsubscribeState: () => void - constructor(connection: KiloConnectionService) { - this.unsubscribeEvent = connection.onEvent((event) => this.handle(event)) + constructor( + connection: KiloConnectionService, + private readonly opts: Options = {}, + ) { + this.unsubscribeEvent = connection.onEvent((event, directory) => this.handle(event, directory)) this.unsubscribeState = connection.onStateChange((state) => { if (state === "error" || state === "disconnected") this.reset() }) @@ -35,35 +42,30 @@ export class AttentionService implements vscode.Disposable { this.unsubscribeEvent() this.unsubscribeState() this.reset() - this.parents.clear() } - private handle(event: SSEPayload) { + private handle(event: SSEPayload, directory?: string) { if (event.type === "sync") return this.sync(event) if (event.type === "question.asked" || event.type === "question.replied" || event.type === "question.rejected") { return this.question(event) } - if (event.type === "permission.asked" || event.type === "permission.replied") return this.permission(event) + if (event.type === "permission.asked" || event.type === "permission.replied") { + return this.permission(event, directory) + } + if (event.type === "session.deleted") return this.remove(event.properties.sessionID) if (event.type === "session.status") return this.status(event) if (event.type === "session.turn.close") return this.close(event) if (event.type === "session.error") return this.error(event) } private sync(event: Sync) { - if (event.name === "session.created.1") { - this.parents.set(event.data.sessionID, event.data.info.parentID) - return - } - if (event.name === "session.updated.1") { - if (event.data.info.parentID !== undefined) { - this.parents.set(event.data.sessionID, event.data.info.parentID ?? undefined) - } - return - } if (event.name !== "session.deleted.1") return - this.parents.delete(event.data.sessionID) - this.active.delete(event.data.sessionID) - this.errored.delete(event.data.sessionID) + this.remove(event.data.sessionID) + } + + private remove(sessionID: string) { + this.active.delete(sessionID) + this.errored.delete(sessionID) } private question(event: Question) { @@ -76,14 +78,24 @@ export class AttentionService implements vscode.Disposable { this.notify("question") } - private permission(event: Permission) { + private permission(event: Permission, directory?: string) { if (event.type !== "permission.asked") { this.permissions.delete(event.properties.requestID) return } - if (this.permissions.has(event.properties.id)) return - this.permissions.add(event.properties.id) - this.notify("permission") + const id = event.properties.id + if (this.permissions.has(id)) return + this.permissions.add(id) + const alert = () => { + if (!this.permissions.has(id)) return + this.notify("permission") + } + const approval = this.opts.approve?.(event, directory) + if (approval === true) return + if (approval === false || approval === undefined) return alert() + void approval.then((handled) => { + if (!handled) alert() + }, alert) } private status(event: Status) { @@ -98,7 +110,8 @@ export class AttentionService implements vscode.Disposable { if (!this.active.delete(sessionID)) return if (this.errored.delete(sessionID)) return if (event.properties.reason !== "completed") return - this.notify(this.parents.get(sessionID) ? "subagent_done" : "done") + if (event.properties.parentID !== undefined) return + this.notify("done") } private error(event: Error) { diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 7a2eeaebbaf..4e33d81992d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -12,10 +12,24 @@ import { NextEditSuggestionManager } from "./next-edit/NextEditSuggestionManager import { toAllowedMercuryRecentSnippets } from "./next-edit/recentSnippetsAdapter" import type { KiloConnectionService } from "../cli-backend" import { hasValidCredentials } from "./fim" -import { DEFAULT_AUTOCOMPLETE_MODEL, getAutocompleteModel } from "../../shared/autocomplete-models" +import { + DEFAULT_AUTOCOMPLETE_MODEL, + getAutocompleteModel, + getAutocompleteModelById, +} from "../../shared/autocomplete-models" const CONFIG_SECTION = "kilo-code.new.autocomplete" +export function selector(kind: "classic" | "next-edit"): vscode.DocumentSelector { + return kind === "classic" ? [{ scheme: "file" }, { scheme: "vscode-notebook-cell" }] : [{ scheme: "file" }] +} + +export function notebookModel(provider?: string, model?: string) { + const info = getAutocompleteModel(provider, model) + if (info.kind !== "edit") return info + return getAutocompleteModelById(info.fimModelID) +} + export interface AutocompleteServiceSettings { enableAutoTrigger?: boolean enableSmartInlineTaskKeybinding?: boolean @@ -68,6 +82,7 @@ export class AutocompleteServiceManager { public readonly nextEditProvider: NextEditInlineCompletionProvider public readonly nextEditSuggestionManager: NextEditSuggestionManager private inlineCompletionProviderDisposable: vscode.Disposable | null = null + private notebookCompletionProviderDisposable: vscode.Disposable | null = null private inlineCompletionProviderKind: "classic" | "next-edit" | null = null private unsubscribeState: (() => void) | null = null private unsubscribeEvent: (() => void) | null = null @@ -177,7 +192,7 @@ export class AutocompleteServiceManager { public async load() { this.settings = readSettings() - this.inlineCompletionProvider.setModel(getAutocompleteModel(this.settings.provider, this.settings.model).id) + this.inlineCompletionProvider.setModel(notebookModel(this.settings.provider, this.settings.model).id) await this.updateGlobalContext() this.updateStatusBar() @@ -204,7 +219,9 @@ export class AutocompleteServiceManager { ) { if (this.inlineCompletionProviderKind === "next-edit") this.nextEditSuggestionManager.clear() this.inlineCompletionProviderDisposable?.dispose() + this.notebookCompletionProviderDisposable?.dispose() this.inlineCompletionProviderDisposable = null + this.notebookCompletionProviderDisposable = null this.inlineCompletionProviderKind = null } @@ -216,7 +233,9 @@ export class AutocompleteServiceManager { if (!shouldBeRegistered) { this.inlineCompletionProviderDisposable!.dispose() + this.notebookCompletionProviderDisposable?.dispose() this.inlineCompletionProviderDisposable = null + this.notebookCompletionProviderDisposable = null this.inlineCompletionProviderKind = null return } @@ -224,9 +243,15 @@ export class AutocompleteServiceManager { const provider: vscode.InlineCompletionItemProvider = desiredKind === "next-edit" ? this.nextEditProvider : this.inlineCompletionProvider this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( - { scheme: "file" }, + selector(desiredKind), provider, ) + if (desiredKind === "next-edit") { + this.notebookCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( + [{ scheme: "vscode-notebook-cell" }], + this.inlineCompletionProvider, + ) + } this.inlineCompletionProviderKind = desiredKind } @@ -459,6 +484,8 @@ export class AutocompleteServiceManager { this.inlineCompletionProviderDisposable = null this.inlineCompletionProviderKind = null } + this.notebookCompletionProviderDisposable?.dispose() + this.notebookCompletionProviderDisposable = null // Dispose inline completion provider resources this.inlineCompletionProvider.dispose() diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts index 5fc37a76bf8..d5e93a6969a 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts @@ -251,12 +251,41 @@ describe("AutocompleteServiceManager (less mocked logic)", () => { await (manager as any).ensureInlineCompletionProviderRegistration() expect(vscode.languages.registerInlineCompletionItemProvider).toHaveBeenCalledWith( - { scheme: "file" }, + [{ scheme: "file" }, { scheme: "vscode-notebook-cell" }], manager.inlineCompletionProvider, ) expect((manager as any).inlineCompletionProviderDisposable).toBe(disposable) }) + it("registers classic notebook fallback when Next Edit is selected", async () => { + const manager = await createManager() + + const primary = { dispose: vi.fn() } + const notebook = { dispose: vi.fn() } + vi.mocked(vscode.languages.registerInlineCompletionItemProvider) + .mockReturnValueOnce(primary as any) + .mockReturnValueOnce(notebook as any) + ;(manager as any).settings = { + enableAutoTrigger: true, + provider: "kilo", + model: "inception/mercury-next-edit", + } + + await (manager as any).ensureInlineCompletionProviderRegistration() + + expect(vscode.languages.registerInlineCompletionItemProvider).toHaveBeenNthCalledWith( + 1, + [{ scheme: "file" }], + manager.nextEditProvider, + ) + expect(vscode.languages.registerInlineCompletionItemProvider).toHaveBeenNthCalledWith( + 2, + [{ scheme: "vscode-notebook-cell" }], + manager.inlineCompletionProvider, + ) + expect((manager as any).notebookCompletionProviderDisposable).toBe(notebook) + }) + it("does not register the provider when snoozed", async () => { const manager = await createManager() diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts index 9d2c0d00823..369906ea4a1 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -7,7 +7,7 @@ import { VisibleCodeTracker } from "../context/VisibleCodeTracker" import { FileIgnoreController } from "../shims/FileIgnoreController" import type { KiloConnectionService } from "../../cli-backend" import { generateFim, hasValidCredentials } from "../fim" -import { getAutocompleteModel } from "../../../shared/autocomplete-models" +import { getAutocompleteModel, getAutocompleteModelById } from "../../../shared/autocomplete-models" import { finalizeChatSuggestion, buildChatPrefix } from "./chat-autocomplete-utils" interface ChatCompletionRequestMessage { @@ -20,6 +20,12 @@ interface ChatCompletionResponseSender { postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void } +export function getChatAutocompleteModel(provider?: string, model?: string) { + const info = getAutocompleteModel(provider, model) + if (info.kind !== "edit") return info + return getAutocompleteModelById(info.fimModelID) +} + /** * Chat textarea autocomplete with cached per-request objects. * @@ -77,7 +83,7 @@ export class ChatTextAreaAutocomplete { async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> { const cfg = vscode.workspace.getConfiguration("kilo-code.new.autocomplete") - const entry = getAutocompleteModel(cfg.get("provider"), cfg.get("model")) + const entry = getChatAutocompleteModel(cfg.get("provider"), cfg.get("model")) const startTime = Date.now() // Build context for telemetry diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts index cc38a899e43..1fd3128145d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts @@ -15,6 +15,7 @@ import { } from "../types" import { findMatchingSuggestion as _findMatchingSuggestion, + findCoveringPendingRequest, applyFirstLineOnly as _applyFirstLineOnly, calcDebounceDelay, MatchingSuggestionWithFillIn as _MatchingSuggestionWithFillIn, @@ -32,10 +33,21 @@ import { postprocessAutocompleteSuggestion } from "./uselessSuggestionFilter" import { shouldSkipAutocomplete } from "./contextualSkip" import { FileIgnoreController } from "../shims/FileIgnoreController" import { AutocompleteTelemetry } from "./AutocompleteTelemetry" +import { + autocompleteScope, + getNotebookContext, + notebookUri, + supportsNotebook, +} from "../continuedev/core/autocomplete/notebook" import { ErrorBackoff } from "./ErrorBackoff" const MAX_SUGGESTIONS_HISTORY = 20 +export function accessible(controller: FileIgnoreController, document: vscode.TextDocument): boolean { + const uri = notebookUri(document.uri) + return controller.validateAccess(uri?.fsPath ?? document.fileName) +} + /** * Minimum debounce delay in milliseconds. * The adaptive debounce delay will never go below this value, even when @@ -70,11 +82,12 @@ export type { CostTrackingCallback, AutocompletePrompt, MatchingSuggestionResult export type MatchingSuggestionWithFillIn = _MatchingSuggestionWithFillIn export function findMatchingSuggestion( + scope: string, prefix: string, suffix: string, suggestionsHistory: FillInAtCursorSuggestion[], ): MatchingSuggestionWithFillIn | null { - return _findMatchingSuggestion(prefix, suffix, suggestionsHistory) + return _findMatchingSuggestion(scope, prefix, suffix, suggestionsHistory) } export function applyFirstLineOnly( @@ -118,8 +131,8 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple private debouncedPendingRequest: PendingRequest | null = null private isFirstCall: boolean = true public readonly ignoreController: Promise - /** Abort controller for the current in-flight FIM request */ - private fimAbortController: AbortController | null = null + /** Abort controllers for in-flight FIM requests, scoped by file/notebook context. */ + private fimAbortControllers = new Map() private acceptedCommand: vscode.Disposable | null = null private contextService: ContextRetrievalService | null = null private debounceDelayMs: number = INITIAL_DEBOUNCE_DELAY_MS @@ -178,6 +191,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple public updateSuggestions(fillInAtCursor: FillInAtCursorSuggestion): void { const isDuplicate = this.suggestionsHistory.some( (existing) => + existing.scope === fillInAtCursor.scope && existing.text === fillInAtCursor.text && existing.prefix === fillInAtCursor.prefix && existing.suffix === fillInAtCursor.suffix, @@ -211,7 +225,16 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple recentlyEditedRanges, } - const autocompleteInput = contextToAutocompleteInput(context) + const input = contextToAutocompleteInput(context) + const notebook = getNotebookContext(document, position) + const autocompleteInput = notebook + ? { + ...input, + filepath: notebook.filepath, + pos: { line: notebook.position.line, character: notebook.position.character }, + manuallyPassFileContents: notebook.contents, + } + : input const { prefix, suffix } = extractPrefixSuffix(document, position) const languageId = document.languageId @@ -235,6 +258,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple private processSuggestion( suggestionText: string, + scope: string, prefix: string, suffix: string, telemetryContext: AutocompleteContext, @@ -242,7 +266,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple ): FillInAtCursorSuggestion { if (!suggestionText) { this.telemetry?.captureSuggestionFiltered("empty_response", telemetryContext) - return { text: "", prefix, suffix } + return { text: "", scope, prefix, suffix } } const processedText = postprocessAutocompleteSuggestion({ @@ -254,11 +278,11 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple }) if (processedText) { - return { text: processedText, prefix, suffix } + return { text: processedText, scope, prefix, suffix } } this.telemetry?.captureSuggestionFiltered("filtered_by_postprocessing", telemetryContext) - return { text: "", prefix, suffix } + return { text: "", scope, prefix, suffix } } private async disposeIgnoreController(): Promise { @@ -299,8 +323,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } this.settleDebouncedPendingRequest() this.pendingRequests.length = 0 - this.fimAbortController?.abort() - this.fimAbortController = null + for (const controller of this.fimAbortControllers.values()) { + controller.abort() + } + this.fimAbortControllers.clear() this.telemetry?.dispose() this.contextService?.dispose() this.contextService = null @@ -336,6 +362,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple _token: vscode.CancellationToken, ): Promise { vscode.commands.executeCommand("setContext", "kilo-code.new.autocomplete.hasSuggestions", false) + if (!supportsNotebook(document)) return [] // Build telemetry context const telemetryContext: AutocompleteContext = { @@ -389,8 +416,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple return [] } - const isAccessible = controller.validateAccess(document.fileName) - if (!isAccessible) { + if (!accessible(controller, document)) { return [] } } catch { @@ -399,10 +425,14 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } } + const scope = autocompleteScope(document) const { prefix, suffix } = extractPrefixSuffix(document, position) // Check cache first - allow mid-word lookups from cache - const matchingResult = applyFirstLineOnly(findMatchingSuggestion(prefix, suffix, this.suggestionsHistory), prefix) + const matchingResult = applyFirstLineOnly( + findMatchingSuggestion(scope, prefix, suffix, this.suggestionsHistory), + prefix, + ) if (matchingResult !== null) { this.lastSuggestion = { @@ -425,9 +455,12 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple const { prompt, prefix: promptPrefix, suffix: promptSuffix } = await this.getPrompt(document, position) - await this.debouncedFetchAndCacheSuggestion(prompt, promptPrefix, promptSuffix, document.languageId) + await this.debouncedFetchAndCacheSuggestion(scope, prompt, promptPrefix, promptSuffix, document.languageId) - const cachedResult = applyFirstLineOnly(findMatchingSuggestion(prefix, suffix, this.suggestionsHistory), prefix) + const cachedResult = applyFirstLineOnly( + findMatchingSuggestion(scope, prefix, suffix, this.suggestionsHistory), + prefix, + ) if (cachedResult) { this.lastSuggestion = { ...telemetryContext, @@ -448,31 +481,6 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } } - /** - * Find a pending request that covers the current prefix/suffix. - * A request covers the current position if: - * 1. The suffix matches (user hasn't changed text after cursor) - * 2. The current prefix either equals or extends the pending prefix - * (user is typing forward, not backspacing or editing earlier) - * - * @returns The covering pending request, or null if none found - */ - private findCoveringPendingRequest(prefix: string, suffix: string): PendingRequest | null { - for (const pendingRequest of this.pendingRequests) { - // Suffix must match exactly (text after cursor unchanged) - if (suffix !== pendingRequest.suffix) { - continue - } - - // Current prefix must start with the pending prefix (user typed more) - // or be exactly equal (same position) - if (prefix.startsWith(pendingRequest.prefix)) { - return pendingRequest - } - } - return null - } - /** * Remove a pending request from the list when it completes. */ @@ -499,13 +507,14 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple * - If a pending request covers the current prefix/suffix, reuse it instead of starting a new one */ private debouncedFetchAndCacheSuggestion( + scope: string, prompt: AutocompletePrompt, prefix: string, suffix: string, languageId: string, ): Promise { // Check if any existing pending request covers this one - const coveringRequest = this.findCoveringPendingRequest(prefix, suffix) + const coveringRequest = findCoveringPendingRequest(scope, prefix, suffix, this.pendingRequests) if (coveringRequest) { // Wait for the existing request to complete - no need to start a new one return coveringRequest.promise @@ -515,8 +524,8 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple // but still track it as a pending request so subsequent calls can reuse it if (this.isFirstCall && this.debounceTimer === null) { this.isFirstCall = false - const promise = this.fetchAndCacheSuggestion(prompt, prefix, suffix, languageId) - const leading: PendingRequest = { prefix, suffix, promise } + const promise = this.fetchAndCacheSuggestion(scope, prompt, prefix, suffix, languageId) + const leading: PendingRequest = { scope, prefix, suffix, promise } promise.finally(() => this.removePendingRequest(leading)) this.pendingRequests.push(leading) return promise @@ -533,6 +542,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple // Create the pending request object first so we can reference it in the cleanup const pendingRequest: PendingRequest = { + scope, prefix, suffix, promise: null!, // Will be set immediately below @@ -545,7 +555,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple this.debouncedPendingRequest = null this.isFirstCall = true // Reset for next sequence try { - await this.fetchAndCacheSuggestion(prompt, prefix, suffix, languageId) + await this.fetchAndCacheSuggestion(scope, prompt, prefix, suffix, languageId) } finally { this.removePendingRequest(pendingRequest) resolve() @@ -566,15 +576,22 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } public async fetchAndCacheSuggestion( + scope: string, prompt: AutocompletePrompt, prefix: string, suffix: string, languageId: string, ): Promise { - // Abort any previous in-flight FIM request before starting a new one - this.fimAbortController?.abort() + // Defense-in-depth: credentials may become invalid between the provider gate and the actual + // debounced execution. In that case, do not attempt an LLM call at all. + if (!hasValidCredentials(this.connectionService)) { + return + } + + // Abort only the request superseded within this file/notebook scope. + this.fimAbortControllers.get(scope)?.abort() const controller = new AbortController() - this.fimAbortController = controller + this.fimAbortControllers.set(scope, controller) const startTime = performance.now() @@ -585,16 +602,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple provider: getAutocompleteModelById(this.contextProvider.modelId).provider, } - // Defense-in-depth: credentials may become invalid between the provider gate and the actual - // debounced execution. In that case, do not attempt an LLM call at all. - if (!hasValidCredentials(this.connectionService)) { - return - } - try { - // Curry processSuggestion with prefix, suffix, telemetry context, and languageId + // Curry processSuggestion with request context const curriedProcessSuggestion = (text: string) => - this.processSuggestion(text, prefix, suffix, telemetryContext, languageId) + this.processSuggestion(text, scope, prefix, suffix, telemetryContext, languageId) const result = await this.fimPromptBuilder.getFromFIM( this.connectionService, @@ -648,6 +659,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple this.fatalNotified = true this.onFatalError?.(this.backoff.getFatalStatus()) } + } finally { + if (this.fimAbortControllers.get(scope) === controller) { + this.fimAbortControllers.delete(scope) + } } } diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/AutocompleteTelemetry.test.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/AutocompleteTelemetry.test.ts index aa5752aed70..e93b4fb50db 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/AutocompleteTelemetry.test.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/AutocompleteTelemetry.test.ts @@ -16,6 +16,7 @@ describe("AutocompleteTelemetry", () => { function createSuggestion(index: number): FillInAtCursorSuggestion { return { text: `text-${index}`, + scope: `scope-${index}`, prefix: `prefix-${index}`, suffix: `suffix-${index}`, } diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/inline-utils.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/inline-utils.ts index b570605e744..74dc958e961 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/inline-utils.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/inline-utils.ts @@ -1,4 +1,4 @@ -import type { FillInAtCursorSuggestion, MatchingSuggestionResult } from "../types" +import type { FillInAtCursorSuggestion, MatchingSuggestionResult, PendingRequest } from "../types" export interface MatchingSuggestionWithFillIn extends MatchingSuggestionResult { fillInAtCursor: FillInAtCursorSuggestion @@ -12,12 +12,14 @@ const MAX_DEBOUNCE_DELAY_MS = 1000 * Searches from most recent to least recent. */ export function findMatchingSuggestion( + scope: string, prefix: string, suffix: string, suggestionsHistory: FillInAtCursorSuggestion[], ): MatchingSuggestionWithFillIn | null { for (let i = suggestionsHistory.length - 1; i >= 0; i--) { const fillInAtCursor = suggestionsHistory[i]! + if (scope !== fillInAtCursor.scope) continue if (prefix === fillInAtCursor.prefix && suffix === fillInAtCursor.suffix) { return { text: fillInAtCursor.text, matchType: "exact", fillInAtCursor } @@ -42,6 +44,19 @@ export function findMatchingSuggestion( return null } +export function findCoveringPendingRequest( + scope: string, + prefix: string, + suffix: string, + requests: PendingRequest[], +): PendingRequest | null { + for (const request of requests) { + if (scope !== request.scope || suffix !== request.suffix) continue + if (prefix.startsWith(request.prefix)) return request + } + return null +} + /** * Counts the number of lines in a text string. * A single trailing newline does not count as an additional line. diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/notebook.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/notebook.ts new file mode 100644 index 00000000000..2dca033b18e --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/notebook.ts @@ -0,0 +1,99 @@ +// Copied from Continue's VS Code autocomplete provider: +// https://github.com/continuedev/continue/blob/d0a3c0b626b5bebc3bef4742eec05a0242be0bab/extensions/vscode/src/autocomplete/completionProvider.ts#L226-L263 +// Copyright 2023 Continue +// Licensed under the Apache License, Version 2.0. +// Modified by Kilo Code for notebook paths, cursor positions, and cache scoping. + +import * as vscode from "vscode" + +export interface NotebookContext { + contents: string + filepath: string + position: vscode.Position +} + +interface NotebookResolution { + notebook: vscode.NotebookDocument + cells: vscode.NotebookCell[] + cell: vscode.NotebookCell + index: number + version: number +} + +const resolutions = new WeakMap() + +function resolveNotebook(uri: vscode.Uri): NotebookResolution | undefined { + const id = uri.toString() + const cached = resolutions.get(uri) + if ( + cached && + cached.notebook.version === cached.version && + vscode.workspace.notebookDocuments.includes(cached.notebook) + ) { + return cached + } + + resolutions.delete(uri) + for (const notebook of vscode.workspace.notebookDocuments) { + const cells = notebook.getCells() + const index = cells.findIndex((cell) => cell.document.uri.toString() === id) + if (index < 0) continue + const resolved = { notebook, cells, cell: cells[index]!, index, version: notebook.version } + resolutions.set(uri, resolved) + return resolved + } +} + +export function notebookUri(uri: vscode.Uri): vscode.Uri | undefined { + if (uri.scheme === "file") return uri + if (uri.scheme !== "vscode-notebook-cell") return + return resolveNotebook(uri)?.notebook.uri +} + +export function supportsNotebook(document: vscode.TextDocument): boolean { + if (document.uri.scheme !== "vscode-notebook-cell") return true + const resolved = resolveNotebook(document.uri) + return resolved?.cell.kind === vscode.NotebookCellKind.Code && document.languageId === "python" +} + +export function autocompleteScope(document: vscode.TextDocument): string { + const id = document.uri.toString() + const resolved = resolveNotebook(document.uri) + if (!resolved) return id + + const siblings = resolved.cells + .filter((_, index) => index !== resolved.index) + .map((cell) => [cell.document.uri.toString(), cell.kind, cell.document.languageId, cell.document.version]) + return JSON.stringify([id, resolved.notebook.uri.toString(), resolved.index, siblings]) +} + +export function getNotebookContext( + document: vscode.TextDocument, + position: vscode.Position, +): NotebookContext | undefined { + if (document.uri.scheme !== "vscode-notebook-cell" || !supportsNotebook(document)) return + + const resolved = resolveNotebook(document.uri) + if (!resolved) return + + const cells = resolved.cells + const contents = cells + .map((cell) => { + const text = cell.document.getText() + if (cell.kind === vscode.NotebookCellKind.Markup) { + return `"""${text}"""` + } + return text + }) + .join("\n\n") + + const line = cells + .slice(0, resolved.index) + .reduce((line, cell) => line + cell.document.getText().split("\n").length + 1, position.line) + + return { + contents, + filepath: resolved.notebook.uri.fsPath, + position: new vscode.Position(line, position.character), + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/types.ts b/packages/kilo-vscode/src/services/autocomplete/types.ts index 765f1d6f4b9..b7a88433828 100644 --- a/packages/kilo-vscode/src/services/autocomplete/types.ts +++ b/packages/kilo-vscode/src/services/autocomplete/types.ts @@ -52,6 +52,7 @@ export interface FillInAtCursorSuggestion { text: string prefix: string suffix: string + scope: string } export interface MatchingSuggestionResult { @@ -105,6 +106,7 @@ export interface LastSuggestionInfo extends AutocompleteContext { } export interface PendingRequest { + scope: string prefix: string suffix: string promise: Promise diff --git a/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts b/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts index 29b53f093da..ee78be1774e 100644 --- a/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts +++ b/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts @@ -1,8 +1,14 @@ import * as fs from "fs" +import * as crypto from "crypto" +import * as os from "os" import * as path from "path" const dir = "tree-sitter" const runtime = "tree-sitter.wasm" +const kiloSandboxWorker = "kilo-sandbox-mutation-worker.js" +const bwrap = "bwrap" +const bwrapLicense = path.join("licenses", "bubblewrap") +const bwrapLicenseFiles = ["NOTICE", "COPYING", "MUSL-COPYRIGHT", "build.ts"] function paths(file: string) { if (/^[a-z]:[\\/]/i.test(file) || file.includes("\\")) return path.win32 @@ -26,6 +32,15 @@ export function hasTreeSitterResources(file: string): boolean { return fs.existsSync(path.join(treeSitterDirForBinary(file), runtime)) } +export function kiloSandboxWorkerForBinary(file: string): string { + const p = paths(file) + return p.join(p.dirname(file), kiloSandboxWorker) +} + +export function hasKiloSandboxWorker(file: string): boolean { + return fs.existsSync(kiloSandboxWorkerForBinary(file)) +} + export async function copyTreeSitterResources(source: string, target: string): Promise { const from = treeSitterDirForBinary(source) const to = treeSitterDirForBinary(target) @@ -37,3 +52,103 @@ export async function copyTreeSitterResources(source: string, target: string): P await fs.promises.rm(to, { recursive: true, force: true }) await fs.promises.cp(from, to, { recursive: true }) } + +export async function copySandboxResources(source: string, target: string): Promise { + const from = path.dirname(source) + const to = path.dirname(target) + const helper = path.join(to, bwrap) + const destination = path.join(to, bwrapLicense) + await fs.promises.rm(helper, { force: true }) + await fs.promises.rm(destination, { recursive: true, force: true }) + + const executable = path.join(from, bwrap) + if (!fs.existsSync(executable)) return + await fs.promises.copyFile(executable, helper) + await fs.promises.chmod(helper, 0o755) + + const licenses = path.join(from, bwrapLicense) + if (!fs.existsSync(licenses)) return + await fs.promises.cp(licenses, destination, { recursive: true }) +} + +export async function copyKiloSandboxWorker(source: string, target: string): Promise { + const from = kiloSandboxWorkerForBinary(source) + const to = kiloSandboxWorkerForBinary(target) + if (!fs.existsSync(from)) throw new Error(`Kilo sandbox mutation worker not found at ${from}`) + await fs.promises.copyFile(from, to) +} + +function cacheRoot() { + const root = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache") + return path.join(root, "kilo-vscode", "bwrap") +} + +function bwrapLicenseDir(bin: string) { + return path.join(bin, bwrapLicense) +} + +function hasBwrapSource(dir: string) { + try { + return fs.readdirSync(dir).some((file) => /^bubblewrap-[a-f0-9]+\.tar\.gz$/.test(file)) + } catch { + return false + } +} + +function hasProductionBwrap(bin: string) { + const executable = path.join(bin, bwrap) + const dir = bwrapLicenseDir(bin) + try { + const entry = fs.statSync(executable) + if (!entry.isFile()) return false + if (!bwrapLicenseFiles.every((file) => fs.existsSync(path.join(dir, file)))) return false + return hasBwrapSource(dir) + } catch { + return false + } +} + +export function localBwrapPath(target: string, root = cacheRoot()): string | undefined { + if (target !== "linux-x64" && target !== "linux-arm64") return undefined + return path.join(root, target, bwrap) +} + +export function localBwrapDigest(file: string): string { + return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") +} + +export function validLocalBwrap(file: string): boolean { + try { + const entry = fs.lstatSync(file) + if (entry.isSymbolicLink() || !entry.isFile() || (entry.mode & 0o6000) !== 0) return false + if ((entry.mode & 0o022) !== 0 || (entry.mode & 0o111) === 0) return false + const digest = fs.readFileSync(`${file}.sha256`, "utf8").trim() + if (!/^[a-f0-9]{64}$/.test(digest)) return false + return localBwrapDigest(file) === digest + } catch { + return false + } +} + +export function resolveLocalBwrapEnv( + extension: string, + local: boolean, + target = `${process.platform === "win32" ? "win32" : process.platform}-${process.arch}`, + root?: string, +): Record { + if (hasProductionBwrap(path.join(extension, "bin"))) return {} + if (!local) return {} + + const executable = localBwrapPath(target, root) + if (!executable || !validLocalBwrap(executable)) return {} + return { KILO_BWRAP_PATH: executable } +} + +export async function sanitizeSandboxResources(bin: string, local: boolean): Promise { + if (hasProductionBwrap(bin)) return true + if (!local) return false + + await fs.promises.rm(path.join(bin, bwrap), { force: true }) + await fs.promises.rm(bwrapLicenseDir(bin), { recursive: true, force: true }) + return false +} diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts index 308beb15bcc..3a2b431b722 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts @@ -20,6 +20,7 @@ export function resolveEventSessionId( } void lookupMessageSessionId + if (event.type === "sandbox.status.changed") return event.properties.sessionID return resolveTransientSessionId(event) } diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts index a2657b33c77..d988b5de65b 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -4,7 +4,7 @@ import * as crypto from "crypto" import * as fs from "fs" import * as path from "path" import * as vscode from "vscode" -import { resolveTreeSitterEnv } from "./cli-resources" +import { resolveLocalBwrapEnv, resolveTreeSitterEnv } from "./cli-resources" import { t } from "./i18n" import { parseServerPort } from "./server-utils" @@ -94,6 +94,10 @@ export class ServerManager { const spawnCwd = resolveServerCwd(folders, this.context.globalStorageUri.fsPath) fs.mkdirSync(spawnCwd, { recursive: true }) const indexingEnv = resolveIndexingEnv(folders) + const localCli = + this.context.extensionMode === vscode.ExtensionMode.Development || + fs.existsSync(path.join(this.context.extensionPath, "bin", ".cli-version")) + const bwrapEnv = process.env.KILO_BWRAP_PATH ? {} : resolveLocalBwrapEnv(this.context.extensionPath, localCli) // TLS / corporate-proxy support: // - Default NODE_USE_SYSTEM_CA=1 so the bundled Bun CLI trusts the OS // trust store (Windows cert store, macOS keychain, Linux /etc/ssl). @@ -141,6 +145,7 @@ export class ServerManager { KILOCODE_EDITOR_NAME: `${vscode.env.appName} ${vscode.version}`, ...(!claudeCompat && { KILO_DISABLE_CLAUDE_CODE: "true" }), ...resolveTreeSitterEnv(this.context.extensionPath), + ...bwrapEnv, }, stdio: ["ignore", "pipe", "pipe"], detached: true, diff --git a/packages/kilo-vscode/src/services/marketplace/detection.ts b/packages/kilo-vscode/src/services/marketplace/detection.ts index fb2030beb7d..d7a3a20eb84 100644 --- a/packages/kilo-vscode/src/services/marketplace/detection.ts +++ b/packages/kilo-vscode/src/services/marketplace/detection.ts @@ -5,6 +5,10 @@ import { MarketplacePaths } from "./paths" type Entry = [string, { type: string }] +function entry(id: string, type: "agent" | "mcp" | "skill"): Entry { + return [`${type}:${id}`, { type }] +} + export interface CliSkill { name: string location: string @@ -52,7 +56,7 @@ export class InstallationDetector { ? !!workspace && this.isProjectSkill(s.location, workspace) : !workspace || !this.isProjectSkill(s.location, workspace), ) - .map((s) => [s.name, { type: "skill" }]) + .map((skill) => entry(skill.name, "skill")) } /** Scan .kilo/agents/*.md files to detect installed marketplace agents. */ @@ -60,7 +64,7 @@ export class InstallationDetector { const dir = this.paths.agentsDir(scope, workspace) try { const files = await fs.readdir(dir) - return files.filter((f) => f.endsWith(".md")).map((f) => [path.basename(f, ".md"), { type: "agent" }] as Entry) + return files.filter((file) => file.endsWith(".md")).map((file) => entry(path.basename(file, ".md"), "agent")) } catch (err) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") { console.warn(`Failed to detect agent files from ${dir}:`, err) @@ -78,13 +82,13 @@ export class InstallationDetector { if (parsed?.mcp && typeof parsed.mcp === "object") { for (const key of Object.keys(parsed.mcp)) { - entries.push([key, { type: "mcp" }]) + entries.push(entry(key, "mcp")) } } if (parsed?.agent && typeof parsed.agent === "object") { for (const key of Object.keys(parsed.agent)) { - entries.push([key, { type: "agent" }]) + entries.push(entry(key, "agent")) } } diff --git a/packages/kilo-vscode/src/services/marketplace/types.ts b/packages/kilo-vscode/src/services/marketplace/types.ts index 81f1c6f031f..1f544860b01 100644 --- a/packages/kilo-vscode/src/services/marketplace/types.ts +++ b/packages/kilo-vscode/src/services/marketplace/types.ts @@ -16,9 +16,9 @@ export interface MarketplaceItemBase { id: string name: string description: string + category: string author?: string authorUrl?: string - tags?: string[] prerequisites?: string[] } @@ -52,7 +52,6 @@ export interface RawSkill { export interface SkillMarketplaceItem extends MarketplaceItemBase { type: "skill" - category: string githubUrl: string content: string displayName: string diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 2f1565ab4df..153eeb1c34e 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -11,8 +11,7 @@ const STORIES = [ { id: "profile--logged-in-personal", name: "Profile / personal account" }, { id: "profile--logged-in", name: "Profile / organization account" }, { id: "settings--providers-configure", name: "Settings / providers empty state" }, - { id: "marketplace--skills-tab-empty", name: "Marketplace / skills empty state" }, - { id: "marketplace--agents-tab-empty", name: "Marketplace / agents empty state" }, + { id: "marketplace--empty-list", name: "Marketplace / empty state" }, { id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" }, ] @@ -49,6 +48,66 @@ test.describe("webview accessibility ratchet", () => { }) } + test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const visibility = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + root.append(layout) + const names = ["assistant-message", "tool-trigger", "file", "code", "diff"] + const values = names.map((name) => { + const node = document.createElement("div") + node.dataset.component = name + layout.append(node) + return getComputedStyle(node).contentVisibility + }) + layout.remove() + return values + }) + + expect(visibility).toEqual(["visible", "visible", "visible", "visible", "visible"]) + }) + + test("Agent Manager avoids generated separator text in updating tool rows", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const content = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + const wrapper = document.createElement("div") + wrapper.dataset.component = "tool-part-wrapper" + wrapper.dataset.partType = "tool" + const collapsible = document.createElement("div") + collapsible.className = "tool-collapsible" + collapsible.dataset.component = "collapsible" + const title = document.createElement("span") + title.dataset.slot = "basic-tool-tool-title" + const subtitle = document.createElement("span") + subtitle.dataset.slot = "basic-tool-tool-subtitle" + collapsible.append(title, subtitle) + wrapper.append(collapsible) + layout.append(wrapper) + root.append(layout) + const value = getComputedStyle(subtitle, "::before").content + layout.remove() + return value + }) + + expect(content).toBe("none") + }) + + test("sidebar keeps transcript announcements while Agent Manager bounds them", async ({ page }) => { + await open(page, "chat--chat-view-with-messages") + await expect(page.locator(".message-list")).toHaveAttribute("role", "log") + await expect(page.locator(".message-list")).toHaveAttribute("aria-live", "polite") + + await open(page, "chat--chat-view-agent-manager-completed") + await expect(page.locator(".message-list")).not.toHaveAttribute("role") + await expect(page.locator(".message-list")).not.toHaveAttribute("aria-live") + await expect(page.locator('.sr-only[role="status"]')).toHaveAttribute("aria-live", "polite") + }) + test("Profile login exposes a keyboard-operable named control", async ({ page }) => { await open(page, "profile--not-logged-in") diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 3104f63016b..ba0cfb51cf9 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.3.46", + "version": "7.3.54", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/settings-accessibility.spec.ts b/packages/kilo-vscode/tests/settings-accessibility.spec.ts index e9c93e86e38..b655b29e610 100644 --- a/packages/kilo-vscode/tests/settings-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/settings-accessibility.spec.ts @@ -31,6 +31,7 @@ test.describe("settings tab accessibility", () => { const tabs = page.getByRole("tab") await expect(tabs).toHaveCount(NAMES.length) + await expect(page.getByRole("tab", { name: "Sandboxing" })).toHaveCount(0) for (const name of NAMES) { await expect(page.getByRole("tab", { name, exact: true })).toBeVisible() } @@ -52,4 +53,22 @@ test.describe("settings tab accessibility", () => { await expect(models).toHaveAttribute("aria-selected", "true") await expect(page.getByRole("tabpanel", { name: "Models" })).toBeVisible() }) + + test("shows sandboxing controls when the feature flag and experiment are enabled", async ({ page }) => { + await page.setViewportSize({ width: 420, height: 720 }) + await page.goto(`/iframe.html?id=settings--sandboxing-panel&viewMode=story&globals=${GLOBALS}`, { + waitUntil: "load", + }) + + const tab = page.getByRole("tab", { name: "Sandboxing" }) + await expect(tab).toBeVisible() + await expect(tab).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Sandboxing" })).toBeVisible() + const network = page.getByRole("switch", { name: "Restrict Network Access" }) + await expect(network).toHaveAccessibleDescription(/Local MCP servers and plugin hooks run outside this restriction/) + await expect(network).toBeChecked() + await page.locator('[data-slot="switch-control"]').click() + await expect(network).not.toBeChecked() + await expect(page.locator(".settings-save-bar")).toBeVisible() + }) }) diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts index 799f0bc4a24..9bef825c60f 100644 --- a/packages/kilo-vscode/tests/setup/vscode-mock.ts +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -47,6 +47,7 @@ const mockVscode = { workspace: { workspaceFolders: [{ uri: { fsPath: "/repo" } }], textDocuments: [] as Array, + notebookDocuments: [] as Array, onDidOpenTextDocument: () => ({ dispose: noop }), onDidChangeTextDocument: () => ({ dispose: noop }), onDidCloseTextDocument: () => ({ dispose: noop }), @@ -73,9 +74,12 @@ const mockVscode = { }, window: { activeTextEditor: undefined, + activeNotebookEditor: undefined, visibleTextEditors: [], + visibleNotebookEditors: [], tabGroups: { all: [] }, showTextDocument: async () => {}, + showInformationMessage: async () => undefined, showWarningMessage: async () => undefined, createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }), createOutputChannel: () => ({ @@ -123,9 +127,19 @@ const mockVscode = { Workspace: 2, WorkspaceFolder: 3, }, + FileType: { + Unknown: 0, + File: 1, + Directory: 2, + SymbolicLink: 64, + }, TabInputText: class { constructor(public uri: { scheme: string; fsPath: string }) {} }, + TabInputNotebook: class { + constructor(public uri: { scheme: string; fsPath: string }) {} + }, + NotebookCellKind: { Markup: 1, Code: 2 }, Position: class { constructor( public line: number, diff --git a/packages/kilo-vscode/tests/unit/abort.test.ts b/packages/kilo-vscode/tests/unit/abort.test.ts index 126333e6a4e..e971a1211b8 100644 --- a/packages/kilo-vscode/tests/unit/abort.test.ts +++ b/packages/kilo-vscode/tests/unit/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import type { KiloClient } from "@kilocode/sdk/v2/client" -import { abortSession } from "../../src/kilo-provider/abort" +import { abortSession, SessionAbort } from "../../src/kilo-provider/abort" function client(calls: unknown[], fail = false) { return { @@ -14,6 +14,53 @@ function client(calls: unknown[], fail = false) { } as unknown as KiloClient } +describe("SessionAbort", () => { + it("stops the active owner and current mapped directory", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(true) + expect(calls).toEqual([ + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo" }, + opts: { throwOnError: true }, + }, + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo/worktree" }, + opts: { throwOnError: true }, + }, + ]) + }) + + it("forgets an owner when its instance becomes idle", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo") + aborts.observe("session_1", "idle", "/repo") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(false) + expect(calls).toEqual([ + { + type: "abort", + params: { sessionID: "session_1", directory: "/repo/worktree" }, + opts: { throwOnError: true }, + }, + ]) + }) + + it("deduplicates equivalent directory paths", async () => { + const calls: unknown[] = [] + const aborts = new SessionAbort() + aborts.observe("session_1", "busy", "/repo/worktree") + + expect(await aborts.stop(client(calls), "session_1", "/repo/worktree/.")).toBe(true) + expect(calls).toHaveLength(1) + }) +}) + describe("abortSession", () => { it("calls session.abort with the session id and directory", async () => { const calls: unknown[] = [] diff --git a/packages/kilo-vscode/tests/unit/attention.test.ts b/packages/kilo-vscode/tests/unit/attention.test.ts index 9c0acd9479a..2e3acb935d9 100644 --- a/packages/kilo-vscode/tests/unit/attention.test.ts +++ b/packages/kilo-vscode/tests/unit/attention.test.ts @@ -5,7 +5,7 @@ import type { KiloConnectionService } from "../../src/services/cli-backend/conne import type { SSEPayload } from "../../src/services/cli-backend/sdk-sse-adapter" import { CustomSoundIDs, resolveSoundID } from "../../src/services/attention/sound" -function setup() { +function setup(opts: { approve?: () => boolean | Promise } = {}) { const sounds: TuiAttentionSoundName[] = [] const events: Array<(event: SSEPayload) => void> = [] const states: Array<(state: "connecting" | "connected" | "disconnected" | "error") => void> = [] @@ -19,7 +19,7 @@ function setup() { return () => undefined }, } as unknown as KiloConnectionService - const service = new AttentionService(connection) + const service = new AttentionService(connection, opts) ;(service as unknown as { notify: (sound: TuiAttentionSoundName) => void }).notify = (sound) => sounds.push(sound) return { sounds, @@ -45,23 +45,20 @@ describe("AttentionService", () => { test.service.dispose() }) - it("uses the upstream subagent completion sound", () => { + it("plays completion sounds only for parent agents", () => { const test = setup() - test.event( - event({ - type: "sync", - name: "session.created.1", - data: { sessionID: "child", info: { parentID: "parent" } }, - }), - ) - test.event( - event({ type: "sync", name: "session.updated.1", data: { sessionID: "child", info: { title: "work" } } }), - ) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "retry" } } })) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "idle" } } })) - test.event(event({ type: "session.turn.close", properties: { sessionID: "child", reason: "completed" } })) + test.event( + event({ + type: "session.turn.close", + properties: { sessionID: "child", parentID: "parent", reason: "completed" }, + }), + ) + test.event(event({ type: "session.status", properties: { sessionID: "parent", status: { type: "busy" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "parent", reason: "completed" } })) - expect(test.sounds).toEqual(["subagent_done"]) + expect(test.sounds).toEqual(["done"]) test.service.dispose() }) @@ -78,6 +75,36 @@ describe("AttentionService", () => { test.service.dispose() }) + it("stays silent for auto-approved permission requests", () => { + const test = setup({ approve: () => true }) + test.event(event({ type: "permission.asked", properties: { id: "p1", sessionID: "s1" } })) + test.event(event({ type: "permission.replied", properties: { requestID: "p1", sessionID: "s1" } })) + + expect(test.sounds).toEqual([]) + test.service.dispose() + }) + + it("plays attention when auto-approval fails and the request remains pending", async () => { + const test = setup({ approve: async () => false }) + test.event(event({ type: "permission.asked", properties: { id: "p1", sessionID: "s1" } })) + await Bun.sleep(0) + + expect(test.sounds).toEqual(["permission"]) + test.service.dispose() + }) + + it("stays silent when a permission resolves before auto-approval failure settles", async () => { + const approval = Promise.withResolvers() + const test = setup({ approve: () => approval.promise }) + test.event(event({ type: "permission.asked", properties: { id: "p1", sessionID: "s1" } })) + test.event(event({ type: "permission.replied", properties: { requestID: "p1", sessionID: "s1" } })) + approval.resolve(false) + await Bun.sleep(0) + + expect(test.sounds).toEqual([]) + test.service.dispose() + }) + it("plays the error sound and suppresses the following completion", () => { const test = setup() test.event(event({ type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } })) @@ -123,6 +150,19 @@ describe("AttentionService", () => { expect(test.sounds).toEqual([]) test.service.dispose() }) + + it("clears transitions when a session is deleted", () => { + const test = setup() + test.event(event({ type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } })) + test.event(event({ type: "session.deleted", properties: { sessionID: "s1", info: { id: "s1" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s1", reason: "completed" } })) + test.event(event({ type: "session.status", properties: { sessionID: "s2", status: { type: "busy" } } })) + test.event(event({ type: "sync", name: "session.deleted.1", data: { sessionID: "s2" } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s2", reason: "completed" } })) + + expect(test.sounds).toEqual([]) + test.service.dispose() + }) }) describe("attention defaults", () => { diff --git a/packages/kilo-vscode/tests/unit/auto-approve.test.ts b/packages/kilo-vscode/tests/unit/auto-approve.test.ts index a10542b9ffa..2f66b71c719 100644 --- a/packages/kilo-vscode/tests/unit/auto-approve.test.ts +++ b/packages/kilo-vscode/tests/unit/auto-approve.test.ts @@ -80,44 +80,37 @@ function context() { } function connection(client: KiloClient | null, dirs = new Map()) { - const listeners: Array<(event: Event, directory?: string) => void> = [] const svc = { getClient: () => { if (!client) throw new Error("not connected") return client }, - onEvent: (listener: (event: Event, directory?: string) => void) => { - listeners.push(listener) - return () => { - const index = listeners.indexOf(listener) - if (index >= 0) listeners.splice(index, 1) - } - }, getPermissionDirectory: (id: string) => dirs.get(id), } as unknown as KiloConnectionService - return { - svc, - emit(event: Event, directory?: string) { - for (const listener of listeners) listener(event, directory) - }, - } + return { svc } } function client(opts: { list?: (dir: string) => Promise<{ data: Permission[] }> - reply?: (args: { requestID: string; directory: string; reply: "once" }) => Promise + reply?: ( + args: { requestID: string; directory: string; reply: "once" }, + options?: { throwOnError?: boolean }, + ) => Promise }) { return { permission: { list: async (args: { directory: string }) => opts.list?.(args.directory) ?? { data: [] }, - reply: async (args: { requestID: string; directory: string; reply: "once" }) => opts.reply?.(args), + reply: async ( + args: { requestID: string; directory: string; reply: "once" }, + options?: { throwOnError?: boolean }, + ) => opts.reply?.(args, options), }, } as unknown as KiloClient } function asked(id: string, sessionID = "ses_1") { - return { type: "permission.asked", properties: { id, sessionID } } as Event + return { type: "permission.asked", properties: { id, sessionID } } as Extract } describe("registerToggleAutoApprove", () => { @@ -135,7 +128,7 @@ describe("registerToggleAutoApprove", () => { ctrl.onChange((active) => changes.push(active)) expect(ctrl.active()).toBe(true) - conn.emit(asked("perm_1")) + expect(await ctrl.approve(asked("perm_1"))).toBe(true) expect(replies).toEqual([{ requestID: "perm_1", directory: "/repo/ses_1", reply: "once" }]) env.active = false @@ -143,7 +136,7 @@ describe("registerToggleAutoApprove", () => { expect(ctrl.active()).toBe(false) expect(changes).toEqual([false]) - conn.emit(asked("perm_2")) + expect(await ctrl.approve(asked("perm_2"))).toBe(false) expect(replies).toHaveLength(1) await ctrl.toggle() @@ -153,19 +146,19 @@ describe("registerToggleAutoApprove", () => { expect(env.messages).toContain("Auto-approve enabled") }) - it("uses the SSE directory for worktree permissions before session mappings are available", () => { + it("uses the SSE directory for worktree permissions before session mappings are available", async () => { config(true) const replies: unknown[] = [] const conn = connection(client({ reply: async (args) => replies.push(args) })) - registerToggleAutoApprove( + const ctrl = registerToggleAutoApprove( context(), conn.svc, () => "/workspace", () => ["/workspace"], ) - conn.emit(asked("perm_worktree", "ses_worktree"), "/workspace/.kilo/worktrees/feature") - conn.emit(asked("perm_child", "ses_child"), "/workspace/.kilo/worktrees/feature") + await ctrl.approve(asked("perm_worktree", "ses_worktree"), "/workspace/.kilo/worktrees/feature") + await ctrl.approve(asked("perm_child", "ses_child"), "/workspace/.kilo/worktrees/feature") expect(replies).toEqual([ { requestID: "perm_worktree", directory: "/workspace/.kilo/worktrees/feature", reply: "once" }, @@ -173,27 +166,47 @@ describe("registerToggleAutoApprove", () => { ]) }) - it("uses the shared permission directory before falling back to session mappings", () => { + it("uses the shared permission directory before falling back to session mappings", async () => { config(true) const replies: unknown[] = [] const conn = connection( client({ reply: async (args) => replies.push(args) }), new Map([["perm_shared", "/workspace/.kilo/worktrees/shared"]]), ) - registerToggleAutoApprove( + const ctrl = registerToggleAutoApprove( context(), conn.svc, () => "/workspace", () => ["/workspace"], ) - conn.emit(asked("perm_shared", "ses_child")) + await ctrl.approve(asked("perm_shared", "ses_child")) expect(replies).toEqual([ { requestID: "perm_shared", directory: "/workspace/.kilo/worktrees/shared", reply: "once" }, ]) }) + it("returns unhandled when an automatic reply fails", async () => { + config(true) + const conn = connection( + client({ + reply: async (_args, options) => { + expect(options).toEqual({ throwOnError: true }) + throw new Error("offline") + }, + }), + ) + const ctrl = registerToggleAutoApprove( + context(), + conn.svc, + () => "/workspace", + () => ["/workspace"], + ) + + expect(await ctrl.approve(asked("perm_1"))).toBe(false) + }) + it("cancels pending permission drains when disabled during an enable generation", async () => { config(false) const gate = defer<{ data: Permission[] }>() @@ -240,6 +253,7 @@ describe("createAutoApproveBridge", () => { const state = { active: false } const ctrl: AutoApproveController = { active: () => state.active, + approve: async () => false, toggle: async () => { state.active = !state.active for (const listener of listeners) listener(state.active) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-abort-scope.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-abort-scope.test.ts new file mode 100644 index 00000000000..18d6b37ce8c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-abort-scope.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "bun:test" +import * as vscode from "vscode" +import { AutocompleteInlineCompletionProvider } from "../../src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider" + +function createProvider(state = "connected") { + ;(vscode.window as any).onDidChangeActiveTextEditor = () => ({ dispose: () => {} }) + ;(vscode.window as any).onDidChangeTextEditorSelection = () => ({ dispose: () => {} }) + return new AutocompleteInlineCompletionProvider( + {} as any, + "kilo/mistralai/codestral-2508", + { getConnectionState: () => state } as any, + () => {}, + () => ({ enableAutoTrigger: true }), + "/repo", + ) +} + +function prompt() { + return { + prefix: "", + suffix: "", + modelName: "codestral", + completionOptions: {}, + selectedCompletionInfo: undefined, + } as any +} + +async function tick() { + await new Promise((resolve) => setTimeout(resolve, 0)) +} + +describe("autocomplete FIM abort scope", () => { + it("does not abort in-flight requests from another scope", async () => { + const provider = createProvider() + const signals: AbortSignal[] = [] + const resolvers: Array<() => void> = [] + ;(provider as any).fimPromptBuilder = { + getFromFIM: async ( + _connection: unknown, + _model: string, + _prompt: unknown, + process: (text: string) => unknown, + signal: AbortSignal, + ) => { + signals.push(signal) + return new Promise((resolve) => { + resolvers.push(() => { + resolve({ + suggestion: process("value"), + cost: 0, + inputTokens: 0, + outputTokens: 0, + }) + }) + }) + }, + } + + const first = provider.fetchAndCacheSuggestion("scope-a", prompt(), "", "", "python") + await tick() + + const second = provider.fetchAndCacheSuggestion("scope-b", prompt(), "", "", "python") + await tick() + + expect(signals[0]?.aborted).toBe(false) + expect(signals[1]?.aborted).toBe(false) + + resolvers.forEach((resolve) => resolve()) + await Promise.all([first, second]) + provider.dispose() + }) + + it("aborts older in-flight requests in the same scope", async () => { + const provider = createProvider() + const signals: AbortSignal[] = [] + const resolvers: Array<() => void> = [] + ;(provider as any).fimPromptBuilder = { + getFromFIM: async ( + _connection: unknown, + _model: string, + _prompt: unknown, + process: (text: string) => unknown, + signal: AbortSignal, + ) => { + signals.push(signal) + return new Promise((resolve) => { + resolvers.push(() => { + resolve({ + suggestion: process("value"), + cost: 0, + inputTokens: 0, + outputTokens: 0, + }) + }) + }) + }, + } + + const first = provider.fetchAndCacheSuggestion("scope-a", prompt(), "", "", "python") + await tick() + + const second = provider.fetchAndCacheSuggestion("scope-a", prompt(), "", "", "python") + await tick() + + expect(signals[0]?.aborted).toBe(true) + expect(signals[1]?.aborted).toBe(false) + + resolvers.forEach((resolve) => resolve()) + await Promise.all([first, second]) + provider.dispose() + }) + + it("does not retain controllers when credentials are invalid", async () => { + const provider = createProvider("disconnected") + ;(provider as any).fimPromptBuilder = { + getFromFIM: () => { + throw new Error("should not fetch") + }, + } + + await provider.fetchAndCacheSuggestion("scope-a", prompt(), "", "", "python") + + expect((provider as any).fimAbortControllers.size).toBe(0) + provider.dispose() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-inline-utils.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-inline-utils.test.ts index 7f171f84b78..4ccae8102b8 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-inline-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-inline-utils.test.ts @@ -6,11 +6,14 @@ import { shouldShowOnlyFirstLine, getFirstLine, calcDebounceDelay, + findCoveringPendingRequest, } from "../../src/services/autocomplete/classic-auto-complete/inline-utils" -import type { FillInAtCursorSuggestion } from "../../src/services/autocomplete/types" +import type { FillInAtCursorSuggestion, PendingRequest } from "../../src/services/autocomplete/types" -function makeSuggestion(prefix: string, text: string, suffix = ""): FillInAtCursorSuggestion { - return { prefix, suffix, text } +const scope = "file:///workspace/file.ts" + +function makeSuggestion(prefix: string, text: string, suffix = "", value = scope): FillInAtCursorSuggestion { + return { scope: value, prefix, suffix, text } } describe("countLines", () => { @@ -89,46 +92,68 @@ describe("shouldShowOnlyFirstLine", () => { describe("findMatchingSuggestion", () => { it("returns null for empty history", () => { - expect(findMatchingSuggestion("prefix", "suffix", [])).toBeNull() + expect(findMatchingSuggestion(scope, "prefix", "suffix", [])).toBeNull() }) it("returns exact match", () => { const hist = [makeSuggestion("hello ", "world")] - const result = findMatchingSuggestion("hello ", "", hist) + const result = findMatchingSuggestion(scope, "hello ", "", hist) expect(result?.matchType).toBe("exact") expect(result?.text).toBe("world") }) it("returns partial_typing match when user typed beginning of suggestion", () => { const hist = [makeSuggestion("he", "llo world")] - const result = findMatchingSuggestion("hell", "", hist) + const result = findMatchingSuggestion(scope, "hell", "", hist) expect(result?.matchType).toBe("partial_typing") expect(result?.text).toBe("o world") }) it("returns backward_deletion match when user deleted chars", () => { const hist = [makeSuggestion("hello world", "more", "suffix")] - const result = findMatchingSuggestion("hello", "suffix", hist) + const result = findMatchingSuggestion(scope, "hello", "suffix", hist) expect(result?.matchType).toBe("backward_deletion") expect(result?.text).toBe(" worldmore") }) it("prefers most recent suggestion (searches from end)", () => { const hist = [makeSuggestion("prefix", "old suggestion"), makeSuggestion("prefix", "new suggestion")] - const result = findMatchingSuggestion("prefix", "", hist) + const result = findMatchingSuggestion(scope, "prefix", "", hist) expect(result?.text).toBe("new suggestion") }) it("returns null when no match found", () => { const hist = [makeSuggestion("different", "no match")] - expect(findMatchingSuggestion("unrelated", "suffix", hist)).toBeNull() + expect(findMatchingSuggestion(scope, "unrelated", "suffix", hist)).toBeNull() }) it("does not match empty suggestion text for partial_typing", () => { const hist = [makeSuggestion("prefix", "")] - const result = findMatchingSuggestion("prefix more", "", hist) + const result = findMatchingSuggestion(scope, "prefix more", "", hist) expect(result).toBeNull() }) + + it("does not reuse suggestions from another scope", () => { + const hist = [makeSuggestion("value = ", "first()", "", "vscode-notebook-cell:notebook.ipynb#cell-a")] + const result = findMatchingSuggestion("vscode-notebook-cell:notebook.ipynb#cell-b", "value = ", "", hist) + expect(result).toBeNull() + }) +}) + +describe("findCoveringPendingRequest", () => { + function request(value: string): PendingRequest { + return { scope: value, prefix: "pri", suffix: "", promise: Promise.resolve() } + } + + it("reuses forward-typing requests within the same scope", () => { + const pending = request(scope) + expect(findCoveringPendingRequest(scope, "print", "", [pending])).toBe(pending) + }) + + it("does not reuse requests from another scope", () => { + const pending = request("vscode-notebook-cell:notebook.ipynb#cell-a") + expect(findCoveringPendingRequest("vscode-notebook-cell:notebook.ipynb#cell-b", "print", "", [pending])).toBeNull() + }) }) describe("applyFirstLineOnly", () => { @@ -138,21 +163,21 @@ describe("applyFirstLineOnly", () => { it("returns empty result unchanged", () => { const hist = [makeSuggestion("p", "")] - const result = findMatchingSuggestion("p", "", hist)! + const result = findMatchingSuggestion(scope, "p", "", hist)! const applied = applyFirstLineOnly(result, "p") expect(applied?.text).toBe("") }) it("truncates to first line when mid-line suggestion", () => { const hist = [makeSuggestion("function foo() { return ", "x\n const y = 1\n}")] - const result = findMatchingSuggestion("function foo() { return ", "", hist)! + const result = findMatchingSuggestion(scope, "function foo() { return ", "", hist)! const applied = applyFirstLineOnly(result, "function foo() { return ") expect(applied?.text).toBe("x") }) it("preserves full multi-line suggestion when starting with newline", () => { const hist = [makeSuggestion("foo", "\n const x = 1\n const y = 2")] - const result = findMatchingSuggestion("foo", "", hist)! + const result = findMatchingSuggestion(scope, "foo", "", hist)! const applied = applyFirstLineOnly(result, "foo") expect(applied?.text).toBe("\n const x = 1\n const y = 2") }) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-selector.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-selector.test.ts new file mode 100644 index 00000000000..2293a531cce --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-selector.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "bun:test" +import { notebookModel, selector } from "../../src/services/autocomplete/AutocompleteServiceManager" + +describe("autocomplete document selector", () => { + it("registers classic autocomplete for files and notebook cells", () => { + expect(selector("classic")).toEqual([{ scheme: "file" }, { scheme: "vscode-notebook-cell" }]) + }) + + it("keeps Next Edit limited to files", () => { + expect(selector("next-edit")).toEqual([{ scheme: "file" }]) + }) + + it("uses the matching FIM model for notebook fallback", () => { + expect(notebookModel("kilo", "inception/mercury-next-edit").id).toBe("kilo/inception/mercury-edit-2") + expect(notebookModel("inception", "mercury-next-edit").id).toBe("inception/mercury-edit-2") + expect(notebookModel("kilo", "mistralai/codestral-2508").id).toBe("kilo/mistralai/codestral-2508") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/autocomplete-telemetry-utils.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-telemetry-utils.test.ts index 9848c21c9e0..3c5ad8d7ab4 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-telemetry-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-telemetry-utils.test.ts @@ -6,29 +6,29 @@ import { describe("getSuggestionKey", () => { it("combines prefix, suffix, and text with pipe separators", () => { - const key = getSuggestionKey({ prefix: "hello ", suffix: "\n}", text: "world" }) + const key = getSuggestionKey({ scope: "", prefix: "hello ", suffix: "\n}", text: "world" }) expect(key).toBe("hello |\n}|world") }) it("produces unique keys for different suggestions", () => { - const k1 = getSuggestionKey({ prefix: "a", suffix: "c", text: "b" }) - const k2 = getSuggestionKey({ prefix: "a", suffix: "c", text: "x" }) + const k1 = getSuggestionKey({ scope: "", prefix: "a", suffix: "c", text: "b" }) + const k2 = getSuggestionKey({ scope: "", prefix: "a", suffix: "c", text: "x" }) expect(k1).not.toBe(k2) }) it("same content produces same key (stable)", () => { - const s = { prefix: "const x = ", suffix: ";", text: "42" } + const s = { scope: "", prefix: "const x = ", suffix: ";", text: "42" } expect(getSuggestionKey(s)).toBe(getSuggestionKey(s)) }) it("different prefix produces different key", () => { - const k1 = getSuggestionKey({ prefix: "a", suffix: "", text: "t" }) - const k2 = getSuggestionKey({ prefix: "b", suffix: "", text: "t" }) + const k1 = getSuggestionKey({ scope: "", prefix: "a", suffix: "", text: "t" }) + const k2 = getSuggestionKey({ scope: "", prefix: "b", suffix: "", text: "t" }) expect(k1).not.toBe(k2) }) it("handles empty strings", () => { - const key = getSuggestionKey({ prefix: "", suffix: "", text: "" }) + const key = getSuggestionKey({ scope: "", prefix: "", suffix: "", text: "" }) expect(key).toBe("||") }) }) diff --git a/packages/kilo-vscode/tests/unit/bwrap-helper.test.ts b/packages/kilo-vscode/tests/unit/bwrap-helper.test.ts new file mode 100644 index 00000000000..5fe28adffe3 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/bwrap-helper.test.ts @@ -0,0 +1,121 @@ +import { afterEach, describe, expect, it } from "bun:test" +import * as fs from "node:fs/promises" +import * as os from "node:os" +import * as path from "node:path" +import { ensureBwrapForTarget } from "../../script/bwrap-helper" +import { + localBwrapPath, + resolveLocalBwrapEnv, + sanitizeSandboxResources, + validLocalBwrap, +} from "../../src/services/cli-backend/cli-resources" + +const configured = process.env.KILO_BWRAP_PATH + +afterEach(() => { + if (configured === undefined) { + delete process.env.KILO_BWRAP_PATH + return + } + process.env.KILO_BWRAP_PATH = configured +}) + +describe("local Bubblewrap helper", () => { + it("copies the configured helper to a cache outside the extension", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-")) + try { + const source = path.join(root, "source", "bwrap") + const extension = path.join(root, "workspace", "packages", "kilo-vscode") + const cache = path.join(root, "cache") + await fs.mkdir(path.dirname(source), { recursive: true }) + await fs.writeFile(source, "bubblewrap") + process.env.KILO_BWRAP_PATH = source + + const dest = await ensureBwrapForTarget("linux-x64", cache) + + expect(dest?.startsWith(extension)).toBe(false) + expect(await fs.readFile(dest!, "utf8")).toBe("bubblewrap") + expect((await fs.stat(dest!)).mode & 0o111).not.toBe(0) + expect(resolveLocalBwrapEnv(extension, true, "linux-x64", cache)).toEqual({ KILO_BWRAP_PATH: dest }) + expect(resolveLocalBwrapEnv(extension, false, "linux-x64", cache)).toEqual({}) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + + it("prefers a complete production helper bundled beside the CLI", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-")) + try { + const extension = path.join(root, "extension") + const bin = path.join(extension, "bin") + const licenses = path.join(bin, "licenses", "bubblewrap") + await fs.mkdir(licenses, { recursive: true }) + await fs.writeFile(path.join(bin, "bwrap"), "bundled") + for (const file of ["NOTICE", "COPYING", "MUSL-COPYRIGHT", "build.ts"]) { + await fs.writeFile(path.join(licenses, file), file) + } + await fs.writeFile(path.join(licenses, "bubblewrap-deadbeef.tar.gz"), "source") + + expect(resolveLocalBwrapEnv(extension, true, "linux-x64", path.join(root, "cache"))).toEqual({}) + expect(await sanitizeSandboxResources(bin, true)).toBe(true) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + + it("removes an incomplete helper before local packaging", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-")) + try { + const bin = path.join(root, "extension", "bin") + const helper = path.join(bin, "bwrap") + await fs.mkdir(bin, { recursive: true }) + await fs.writeFile(helper, "stale") + + expect(await sanitizeSandboxResources(bin, true)).toBe(false) + expect( + await fs.stat(helper).then( + () => true, + () => false, + ), + ).toBe(false) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + + it("rejects a symlinked or public cache", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-")) + try { + const source = path.join(root, "source") + const cache = path.join(root, "cache") + const dest = localBwrapPath("linux-x64", cache)! + await fs.writeFile(source, "bubblewrap") + await fs.mkdir(path.dirname(dest), { recursive: true }) + await fs.symlink(source, dest) + await fs.writeFile(`${dest}.sha256`, "0".repeat(64)) + expect(validLocalBwrap(dest)).toBe(false) + + await fs.rm(cache, { recursive: true, force: true }) + await fs.mkdir(cache, { recursive: true, mode: 0o777 }) + await fs.chmod(cache, 0o777) + process.env.KILO_BWRAP_PATH = source + await expect(ensureBwrapForTarget("linux-x64", cache)).rejects.toThrow("cache directory is not private") + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + + it("does not stage Bubblewrap for unsupported operating systems", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-")) + try { + process.env.KILO_BWRAP_PATH = path.join(root, "missing") + + const dest = await ensureBwrapForTarget("darwin-arm64", path.join(root, "cache")) + + expect(dest).toBeUndefined() + expect(resolveLocalBwrapEnv(path.join(root, "extension"), true, "darwin-arm64", root)).toEqual({}) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) +}) diff --git a/packages/kilo-vscode/tests/unit/chat-autocomplete-utils.test.ts b/packages/kilo-vscode/tests/unit/chat-autocomplete-utils.test.ts index 415362ab7e5..216c574d1d7 100644 --- a/packages/kilo-vscode/tests/unit/chat-autocomplete-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/chat-autocomplete-utils.test.ts @@ -3,6 +3,7 @@ import { finalizeChatSuggestion, buildChatPrefix, } from "../../src/services/autocomplete/chat-autocomplete/chat-autocomplete-utils" +import { getChatAutocompleteModel } from "../../src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete" describe("finalizeChatSuggestion", () => { it("returns empty string for empty input", () => { @@ -98,3 +99,14 @@ describe("buildChatPrefix", () => { expect(result).toContain("hi") }) }) + +describe("getChatAutocompleteModel", () => { + it("uses the matching FIM model for Next Edit settings", () => { + expect(getChatAutocompleteModel("kilo", "inception/mercury-next-edit").id).toBe("kilo/inception/mercury-edit-2") + expect(getChatAutocompleteModel("inception", "mercury-next-edit").id).toBe("inception/mercury-edit-2") + }) + + it("keeps FIM settings unchanged", () => { + expect(getChatAutocompleteModel("kilo", "mistralai/codestral-2508").id).toBe("kilo/mistralai/codestral-2508") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/connection-utils.test.ts b/packages/kilo-vscode/tests/unit/connection-utils.test.ts index 46d74e8a231..2970f9eb05e 100644 --- a/packages/kilo-vscode/tests/unit/connection-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/connection-utils.test.ts @@ -158,6 +158,16 @@ describe("resolveEventSessionId", () => { expect(resolveEventSessionId(suggestion, noLookup)).toBe("s10") }) + it("routes sandbox status events", () => { + const event = { + id: "e12", + type: "sandbox.status.changed", + properties: { sessionID: "s11", directory: "/repo", enabled: true, available: true, version: 1 }, + } satisfies Payload + + expect(resolveEventSessionId(event, noLookup)).toBe("s11") + }) + it("returns undefined for global events", () => { const event = { id: "e12", type: "server.connected", properties: {} } satisfies Payload diff --git a/packages/kilo-vscode/tests/unit/indexing-settings-message.test.ts b/packages/kilo-vscode/tests/unit/indexing-settings-message.test.ts new file mode 100644 index 00000000000..b80f479de07 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/indexing-settings-message.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import * as vscode from "vscode" +import { buildIndexingSettingsMessage, validIndexingSetting } from "../../src/kilo-provider/indexing-settings" + +type Stub = { + getConfiguration: (section?: string) => { + get: (key: string, fallback?: T) => T | undefined + } +} + +const original = vscode.workspace.getConfiguration + +function stubConfig(state: Map) { + ;(vscode.workspace as unknown as Stub).getConfiguration = (section?: string) => { + if (section !== "kilo-code.new.indexing") { + return { get: (_key: string, fallback?: T) => fallback } + } + return { + get: (key: string, fallback?: T) => (state.has(key) ? (state.get(key) as T) : fallback), + } + } +} + +afterEach(() => { + ;(vscode.workspace as unknown as Stub).getConfiguration = original as Stub["getConfiguration"] +}) + +describe("buildIndexingSettingsMessage", () => { + let state: Map + + beforeEach(() => { + state = new Map() + stubConfig(state) + }) + + it("shows the indexing button by default", () => { + expect(buildIndexingSettingsMessage().settings.showButtonWhenDisabled).toBe(true) + }) + + it("returns the persisted button preference", () => { + state.set("showButtonWhenDisabled", false) + + expect(buildIndexingSettingsMessage().settings.showButtonWhenDisabled).toBe(false) + }) +}) + +describe("validIndexingSetting", () => { + it("accepts only boolean button visibility updates", () => { + expect(validIndexingSetting("showButtonWhenDisabled", true)).toBe(true) + expect(validIndexingSetting("showButtonWhenDisabled", false)).toBe(true) + expect(validIndexingSetting("showButtonWhenDisabled", "false")).toBe(false) + expect(validIndexingSetting("unknown", true)).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts index 0d20e40d656..ad6551e802d 100644 --- a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "bun:test" import { applyIndexingStatusMessage, formatIndexingLabel, + indexingButtonVisible, indexingTone, } from "../../webview-ui/src/context/indexing-utils" import { mapSSEEventToWebviewMessage } from "../../src/kilo-provider-utils" @@ -19,6 +20,41 @@ function makeStatus(overrides: Partial = {}): IndexingStatus { } } +describe("indexing button visibility", () => { + it("hides the button when the indexing feature is unavailable", () => { + expect(indexingButtonVisible(false, true, { indexing: { enabled: true } }, { indexing: { enabled: true } })).toBe( + false, + ) + }) + + it("shows the button while indexing is off by default", () => { + expect(indexingButtonVisible(true, true, {}, {})).toBe(true) + }) + + it("hides the button when indexing is off and the preference is disabled", () => { + expect(indexingButtonVisible(true, false, {}, {})).toBe(false) + }) + + it("shows the button when project indexing is enabled", () => { + expect(indexingButtonVisible(true, false, { indexing: { enabled: true } }, {})).toBe(true) + expect(indexingButtonVisible(true, false, { indexing: { enabled: true } }, { indexing: { enabled: false } })).toBe( + true, + ) + }) + + it("stays hidden when global and project indexing are off", () => { + expect(indexingButtonVisible(true, false, { indexing: { enabled: false } }, { indexing: { enabled: false } })).toBe( + false, + ) + }) + + it("shows the button whenever global indexing is enabled", () => { + expect(indexingButtonVisible(true, false, { indexing: { enabled: false } }, { indexing: { enabled: true } })).toBe( + true, + ) + }) +}) + describe("indexing formatting", () => { it("formats in-progress status like the TUI", () => { const status = makeStatus({ state: "In Progress", percent: 42, processedFiles: 21, totalFiles: 50 }) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts index 3145932a2ca..2a6e8717f13 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-load-messages.test.ts @@ -1,8 +1,9 @@ -import { describe, it, expect } from "bun:test" +import { describe, it, expect, spyOn } from "bun:test" +import * as vscode from "vscode" import type { PartUpdate } from "../../src/shared/stream-messages" // vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) -const { KiloProvider } = await import("../../src/KiloProvider") +const { KiloProvider, unwrapSyncEvent } = await import("../../src/KiloProvider") type State = "connecting" | "connected" | "disconnected" | "error" @@ -34,6 +35,21 @@ function mkMessage(id: string, role: "user" | "assistant", time = 0) { } } +function mkSession(revert?: { messageID: string }) { + return { + id: "s1", + slug: "session", + version: "1", + projectID: "project", + directory: "/repo", + title: "Session", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 1 }, + revert, + } +} + function mkResult(items: unknown[]) { return { data: items, response: { headers: new Headers() } } } @@ -42,21 +58,54 @@ function createClient(options?: { messagesDeferred?: Deferred<{ data: unknown[]; response: { headers: Headers } }> messagesData?: unknown[] deleteDeferred?: Deferred + revertDeferred?: Deferred<{ data?: unknown; error?: unknown }> sessionData?: unknown sessionGet?: (params: { sessionID: string; directory?: string }) => Promise<{ data: unknown }> + abortFailures?: string[] + createDeferred?: Deferred<{ data: unknown }> + sandboxDeferred?: Deferred<{ data: unknown }> + sandboxStarted?: Deferred }) { const calls: { before?: string; limit?: number }[] = [] const stopped: { sessionID: string; directory?: string }[] = [] + const aborted: { sessionID: string; directory?: string }[] = [] + const prompted: Array> = [] + const reverted: Array> = [] + const created: Array> = [] + const sandboxed: Array> = [] return { calls, stopped, + aborted, + prompted, + reverted, + created, + sandboxed, session: { list: async () => ({ data: [] }), + create: async (params: Record) => { + created.push(params) + return options?.createDeferred?.promise ?? { data: mkSession() } + }, get: async (params: { sessionID: string; directory?: string }) => { if (options?.sessionGet) return options.sessionGet(params) return { data: options?.sessionData ?? null } }, status: async () => ({ data: {} }), + revert: async (params: Record) => { + reverted.push(params) + if (options?.revertDeferred) return options.revertDeferred.promise + return { data: mkSession({ messageID: String(params.messageID) }) } + }, + promptAsync: async (params: Record) => { + prompted.push(params) + return { data: undefined } + }, + abort: async (params: { sessionID: string; directory?: string }) => { + aborted.push(params) + if (params.directory && options?.abortFailures?.includes(params.directory)) throw new Error("abort failed") + return { data: true } + }, messages: async (params: { before?: string; limit?: number }) => { calls.push({ before: params.before, limit: params.limit }) if (options?.messagesDeferred) return options.messagesDeferred.promise @@ -67,6 +116,17 @@ function createClient(options?: { return { data: {} } }, }, + sandbox: { + toggle: async (params: Record) => { + sandboxed.push(params) + options?.sandboxStarted?.resolve(undefined) + return ( + options?.sandboxDeferred?.promise ?? { + data: { directory: "/repo", enabled: true, available: true, version: 1 }, + } + ) + }, + }, backgroundProcess: { stopSession: async (params: { sessionID: string; directory?: string }) => { stopped.push(params) @@ -112,13 +172,22 @@ function createConnection(client: ReturnType) { type ProviderInternals = { connectionState: State webview: { postMessage: (message: unknown) => Promise } | null - currentSession: { id: string; directory?: string } | null + currentSession: { id: string; directory?: string; revert?: { messageID: string } } | null contextSessionID: string | undefined sessionDirectories: Map trackedSessionIds: Set + checkpoints: Map> + revisions: Map streams: { push: (msg: PartUpdate) => void } + checkpoint: (sid: string, run: () => Promise) => void + gatherEditorContext: () => Promise> + refreshSessionDetails: (sid: string, dir: string) => void stopCurrentSessionProcesses: (next?: string) => void - handleEvent: (event: unknown) => void + handleEvent: (event: unknown, directory?: string) => void + handleAbort: (sid?: string) => Promise + handleRevertSession: (sid: string, messageID: string) => Promise + handleSendMessage: (text: string, messageID?: string, sessionID?: string, draftID?: string) => Promise + handleToggleSandbox: (input: { sessionID?: string; draftID?: string; requestID: string }) => Promise handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise handleDeleteSession: (sid: string) => Promise } @@ -137,6 +206,465 @@ function makeProvider(client: ReturnType) { return { provider, internal, sent } } +describe("KiloProvider.handleAbort", () => { + it("aborts the original owner after a running session moves to a worktree", async () => { + const client = createClient() + const { provider, internal, sent } = makeProvider(client) + internal.handleEvent( + { + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }, + "/repo", + ) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + expect(sent.at(-1)).toMatchObject({ type: "sessionStatus", sessionID: "s1", status: "idle" }) + }) + + it("preserves the original owner when the status event lacks a directory", async () => { + const client = createClient() + const { provider, internal } = makeProvider(client) + internal.handleEvent({ + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + }) + + it("attempts every owner and stays busy when one abort fails", async () => { + const error = spyOn(console, "error").mockImplementation(() => {}) + const client = createClient({ abortFailures: ["/repo"] }) + const { provider, internal, sent } = makeProvider(client) + internal.handleEvent( + { + type: "session.status", + properties: { sessionID: "s1", status: { type: "busy" } }, + }, + "/repo", + ) + provider.setSessionDirectory("s1", "/repo/worktree") + + await internal.handleAbort("s1") + + expect(client.aborted).toEqual([ + { sessionID: "s1", directory: "/repo" }, + { sessionID: "s1", directory: "/repo/worktree" }, + ]) + expect(sent.at(-1)).toMatchObject({ type: "sessionStatus", sessionID: "s1", status: "busy" }) + expect(error).toHaveBeenCalledTimes(1) + error.mockRestore() + }) +}) + +describe("KiloProvider sandbox status", () => { + it("ignores events from another directory for the same session", () => { + const client = createClient() + const { internal, sent } = makeProvider(client) + internal.sessionDirectories.set("s1", "/repo") + internal.trackedSessionIds.add("s1") + + internal.handleEvent({ + type: "sandbox.status.changed", + properties: { sessionID: "s1", directory: "/other", enabled: true, available: true, version: 1 }, + }) + expect(sent.some((message) => (message as { type?: string }).type === "sandboxStatus")).toBe(false) + + internal.handleEvent({ + type: "sandbox.status.changed", + properties: { sessionID: "s1", directory: "/repo", enabled: true, available: true, version: 1 }, + }) + expect(sent).toContainEqual(expect.objectContaining({ type: "sandboxStatus", sessionID: "s1", directory: "/repo" })) + }) +}) + +describe("KiloProvider sandbox toggle", () => { + it("creates a session before toggling from the empty composer", async () => { + const notice = spyOn(vscode.window, "showInformationMessage").mockResolvedValue(undefined) + const client = createClient() + const { internal, sent } = makeProvider(client) + + await internal.handleToggleSandbox({ draftID: "draft-1", requestID: "sandbox-1" }) + + expect(client.created).toEqual([expect.objectContaining({ directory: "/repo" })]) + expect(client.sandboxed).toEqual([{ sessionID: "s1", directory: "/repo" }]) + expect(sent.findIndex((message) => (message as { type?: string }).type === "sessionCreated")).toBeLessThan( + sent.findIndex((message) => (message as { type?: string }).type === "sandboxStatus"), + ) + expect(sent).toContainEqual( + expect.objectContaining({ + type: "sandboxStatus", + sessionID: "s1", + requestID: "sandbox-1", + enabled: true, + }), + ) + expect(notice).toHaveBeenCalledTimes(1) + expect(notice).toHaveBeenCalledWith("Sandbox enabled") + notice.mockRestore() + }) + + it("reports the disabled state in a native notification", async () => { + const notice = spyOn(vscode.window, "showInformationMessage").mockResolvedValue(undefined) + const sandbox = defer<{ data: unknown }>() + const client = createClient({ sandboxDeferred: sandbox }) + const { internal } = makeProvider(client) + internal.currentSession = mkSession() + + const toggle = internal.handleToggleSandbox({ sessionID: "s1", requestID: "sandbox-1" }) + sandbox.resolve({ data: { directory: "/repo", enabled: false, available: true, version: 2 } }) + await toggle + + expect(notice).toHaveBeenCalledTimes(1) + expect(notice).toHaveBeenCalledWith("Sandbox disabled") + notice.mockRestore() + }) + + it("shares session creation and finishes the toggle before a prompt", async () => { + const create = defer<{ data: unknown }>() + const sandbox = defer<{ data: unknown }>() + const started = defer() + const client = createClient({ createDeferred: create, sandboxDeferred: sandbox, sandboxStarted: started }) + const { internal } = makeProvider(client) + internal.gatherEditorContext = async () => ({}) + + const toggle = internal.handleToggleSandbox({ draftID: "draft-1", requestID: "sandbox-1" }) + await Promise.resolve() + const send = internal.handleSendMessage("hello", "message-1", undefined, "draft-1") + await Promise.resolve() + + expect(client.created).toHaveLength(1) + expect(client.prompted).toHaveLength(0) + + create.resolve({ data: mkSession() }) + await started.promise + expect(client.sandboxed).toHaveLength(1) + expect(client.prompted).toHaveLength(0) + + sandbox.resolve({ data: { directory: "/repo", enabled: true, available: true, version: 1 } }) + await Promise.all([toggle, send]) + expect(client.created).toHaveLength(1) + expect(client.prompted).toHaveLength(1) + }) + + it("does not send a queued prompt when the sandbox toggle fails", async () => { + const log = spyOn(console, "error").mockImplementation(() => {}) + const notice = spyOn(vscode.window, "showInformationMessage").mockResolvedValue(undefined) + const sandbox = defer<{ data: unknown }>() + const started = defer() + const client = createClient({ sandboxDeferred: sandbox, sandboxStarted: started }) + const { internal, sent } = makeProvider(client) + internal.gatherEditorContext = async () => ({}) + + const toggle = internal.handleToggleSandbox({ draftID: "draft-1", requestID: "sandbox-1" }) + await started.promise + const send = internal.handleSendMessage("hello", "message-1", undefined, "draft-1") + sandbox.reject(new Error("toggle failed")) + await Promise.all([toggle, send]) + + expect(client.prompted).toHaveLength(0) + expect(sent).toContainEqual(expect.objectContaining({ type: "sandboxStatusError", requestID: "sandbox-1" })) + expect(sent).toContainEqual( + expect.objectContaining({ type: "sendMessageFailed", sessionID: "s1", messageID: "message-1" }), + ) + expect(notice).not.toHaveBeenCalled() + notice.mockRestore() + log.mockRestore() + }) + + it("does not send a queued prompt when the sandbox backend is unavailable", async () => { + const log = spyOn(console, "error").mockImplementation(() => {}) + const notice = spyOn(vscode.window, "showInformationMessage").mockResolvedValue(undefined) + const sandbox = defer<{ data: unknown }>() + const started = defer() + const client = createClient({ sandboxDeferred: sandbox, sandboxStarted: started }) + const { internal, sent } = makeProvider(client) + internal.gatherEditorContext = async () => ({}) + + const toggle = internal.handleToggleSandbox({ draftID: "draft-1", requestID: "sandbox-1" }) + await started.promise + const send = internal.handleSendMessage("hello", "message-1", undefined, "draft-1") + sandbox.resolve({ + data: { directory: "/repo", enabled: false, available: false, reason: "unsupported", version: 0 }, + }) + await Promise.all([toggle, send]) + + expect(client.prompted).toHaveLength(0) + expect(sent).toContainEqual(expect.objectContaining({ type: "sandboxStatusError", message: "unsupported" })) + expect(sent).toContainEqual( + expect.objectContaining({ type: "sendMessageFailed", sessionID: "s1", messageID: "message-1" }), + ) + expect(notice).not.toHaveBeenCalled() + notice.mockRestore() + log.mockRestore() + }) + + it("keeps prompts queued after the draft is promoted", async () => { + const sandbox = defer<{ data: unknown }>() + const started = defer() + const client = createClient({ sandboxDeferred: sandbox, sandboxStarted: started }) + const { internal } = makeProvider(client) + internal.gatherEditorContext = async () => ({}) + + const toggle = internal.handleToggleSandbox({ draftID: "draft-1", requestID: "sandbox-1" }) + await started.promise + const send = internal.handleSendMessage("hello", "message-1", "s1", "draft-1") + await Promise.resolve() + expect(client.prompted).toHaveLength(0) + + sandbox.resolve({ data: { directory: "/repo", enabled: true, available: true, version: 1 } }) + await Promise.all([toggle, send]) + expect(client.prompted).toHaveLength(1) + }) +}) + +describe("KiloProvider revert ordering", () => { + it("unwraps the nested sync payload emitted by the live SSE endpoint", () => { + const event = unwrapSyncEvent({ + type: "sync", + syncEvent: { + type: "session.updated.1", + id: "evt_clear", + seq: 0, + aggregateID: "sessionID", + data: { sessionID: "s1", info: { revert: null } }, + }, + }) + + expect(event).toEqual({ + source: "sync", + id: "evt_clear", + seq: 0, + type: "session.updated", + properties: { sessionID: "s1", info: { revert: null } }, + }) + }) + + it("waits for an in-flight revert before submitting the replacement prompt", async () => { + const revert = defer<{ data?: unknown; error?: unknown }>() + const client = createClient({ revertDeferred: revert }) + const { internal } = makeProvider(client) + internal.currentSession = mkSession() + internal.gatherEditorContext = async () => ({}) + + internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1")) + const send = internal.handleSendMessage("replacement", "m2", "s1") + await Promise.resolve() + await Promise.resolve() + + expect(client.reverted).toHaveLength(1) + expect(client.prompted).toHaveLength(0) + + revert.resolve({ data: mkSession({ messageID: "m1" }) }) + await send + + expect(client.prompted).toHaveLength(1) + expect(client.prompted[0]?.sessionID).toBe("s1") + }) + + it("waits for a revert queued while the replacement prompt gathers context", async () => { + const context = defer>() + const revert = defer<{ data?: unknown; error?: unknown }>() + const client = createClient({ revertDeferred: revert }) + const { internal } = makeProvider(client) + internal.currentSession = mkSession() + internal.gatherEditorContext = () => context.promise + + const send = internal.handleSendMessage("replacement", "m2", "s1") + await Promise.resolve() + internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1")) + context.resolve({}) + await Promise.resolve() + await Promise.resolve() + + expect(client.prompted).toHaveLength(0) + + revert.resolve({ data: mkSession({ messageID: "m1" }) }) + await send + + expect(client.prompted).toHaveLength(1) + }) + + it("does not submit the replacement prompt when the revert fails", async () => { + const error = spyOn(console, "error").mockImplementation(() => {}) + const revert = defer<{ data?: unknown; error?: unknown }>() + const client = createClient({ revertDeferred: revert }) + const { internal, sent } = makeProvider(client) + internal.currentSession = mkSession() + internal.gatherEditorContext = async () => ({}) + + internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1")) + const send = internal.handleSendMessage("replacement", "m2", "s1") + await Promise.resolve() + revert.resolve({ error: new Error("revert failed") }) + await send + + expect(client.prompted).toHaveLength(0) + expect(sent).toContainEqual(expect.objectContaining({ type: "sendMessageFailed", messageID: "m2" })) + error.mockRestore() + }) + + it("does not restore a stale revert boundary after a newer clear update", () => { + const client = createClient() + const { internal, sent } = makeProvider(client) + internal.currentSession = mkSession({ messageID: "m1" }) + internal.trackedSessionIds.add("s1") + + internal.handleEvent({ + source: "sync", + id: "evt_000000000002", + seq: 0, + type: "session.updated", + properties: { sessionID: "s1", info: { revert: null } }, + }) + const count = sent.length + + internal.handleEvent({ + source: "sync", + id: "evt_000000000001", + seq: 0, + type: "session.updated", + properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } }, + }) + internal.handleEvent({ + id: "evt_000000000001", + type: "session.updated", + properties: { sessionID: "s1", info: mkSession({ messageID: "m1" }) }, + }) + + expect(internal.currentSession?.revert).toBeUndefined() + expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000002", seq: 0 }) + expect(sent).toHaveLength(count) + expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } }) + }) + + it("uses sequence ordering for workspace-replayed session updates", () => { + const client = createClient() + const { internal } = makeProvider(client) + internal.currentSession = mkSession({ messageID: "m1" }) + internal.trackedSessionIds.add("s1") + + internal.handleEvent({ + source: "sync", + id: "evt_ffffffffffff", + seq: 1, + type: "session.updated", + properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } }, + }) + internal.handleEvent({ + source: "sync", + id: "evt_000000000001", + seq: 2, + type: "session.updated", + properties: { sessionID: "s1", info: { revert: null } }, + }) + + expect(internal.currentSession?.revert).toBeUndefined() + expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000001", seq: 2 }) + }) + + it("publishes authoritative session state after a missed clear event", async () => { + const client = createClient({ sessionData: mkSession() }) + const { internal, sent } = makeProvider(client) + internal.currentSession = mkSession({ messageID: "m1" }) + internal.contextSessionID = "s1" + + internal.refreshSessionDetails("s1", "/repo") + await Promise.resolve() + await Promise.resolve() + + expect(internal.currentSession?.revert).toBeUndefined() + expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } }) + }) + + it("retries a focused session refresh after a concurrent session update", async () => { + const first = defer<{ data: unknown }>() + const second = defer<{ data: unknown }>() + let calls = 0 + const client = createClient({ + sessionGet: async () => { + calls += 1 + return calls === 1 ? first.promise : second.promise + }, + }) + const { internal } = makeProvider(client) + internal.currentSession = mkSession({ messageID: "m1" }) + internal.contextSessionID = "s1" + internal.trackedSessionIds.add("s1") + + internal.refreshSessionDetails("s1", "/repo") + internal.handleEvent({ + source: "sync", + id: "evt_000000000001", + seq: 0, + type: "session.updated", + properties: { sessionID: "s1", info: { title: "updated" } }, + }) + first.resolve({ data: mkSession() }) + await Bun.sleep(0) + expect(calls).toBe(2) + + second.resolve({ data: { ...mkSession(), title: "updated" } }) + await Bun.sleep(0) + + expect(internal.currentSession?.id).toBe("s1") + expect(internal.currentSession?.revert).toBeUndefined() + }) + + it("ignores an older session refresh that resolves last", async () => { + const first = defer<{ data: unknown }>() + const second = defer<{ data: unknown }>() + let calls = 0 + const client = createClient({ + sessionGet: async () => { + calls += 1 + return calls === 1 ? first.promise : second.promise + }, + }) + const { internal, sent } = makeProvider(client) + internal.currentSession = mkSession({ messageID: "m1" }) + internal.contextSessionID = "s1" + + internal.refreshSessionDetails("s1", "/repo") + internal.refreshSessionDetails("s1", "/repo") + second.resolve({ data: mkSession() }) + await Bun.sleep(0) + first.resolve({ data: mkSession({ messageID: "m1" }) }) + await Bun.sleep(0) + + expect(internal.currentSession?.revert).toBeUndefined() + expect(sent.filter((msg) => (msg as { type?: string }).type === "sessionUpdated")).toHaveLength(1) + }) + + it("ignores a session refresh superseded by a revert response", async () => { + const session = defer<{ data: unknown }>() + const client = createClient({ sessionGet: async () => session.promise }) + const { internal } = makeProvider(client) + internal.currentSession = mkSession() + internal.contextSessionID = "s1" + + internal.refreshSessionDetails("s1", "/repo") + await internal.handleRevertSession("s1", "m1") + session.resolve({ data: mkSession() }) + await Bun.sleep(0) + + expect(internal.currentSession?.revert).toEqual({ messageID: "m1" }) + }) +}) + describe("KiloProvider.handleLoadMessages / focus mode freshness", () => { it("stops background processes for the previous session when switching sessions", async () => { const client = createClient({ diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts index 2428bbb921d..90526054cc3 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts @@ -24,6 +24,7 @@ import type { SyncEventMessageUpdated, EventSessionStatus, EventSessionTurnClose, + EventSandboxStatusChanged, EventPermissionAsked, EventPermissionReplied, EventTodoUpdated, @@ -448,6 +449,23 @@ describe("mapSSEEventToWebviewMessage", () => { } }) + it("maps sandbox status changes to effective button state", () => { + const event: EventSandboxStatusChanged = { + type: "sandbox.status.changed", + properties: { sessionID: "sess-1", directory: "/tmp", enabled: true, available: true, version: 3 }, + } + + expect(mapSSEEventToWebviewMessage(event, "sess-1")).toEqual({ + type: "sandboxStatus", + sessionID: "sess-1", + directory: "/tmp", + enabled: true, + available: true, + reason: undefined, + version: 3, + }) + }) + it("maps session.turn.close to its terminal reason", () => { const event: EventSessionTurnClose = { id: "evt-turn", diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index f84657ba977..41e4cbd25f2 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -201,8 +201,9 @@ describe("Bash tool static terminal preview (source)", () => { expect(block).toContain("BashHighlightedOutput") }) - it("animates expanded bash details", () => { - expect(block).toMatch(/allowPendingToggle\s+animated\s+trigger=/) + it("does not animate expanded bash details", () => { + expect(block).toMatch(/allowPendingToggle\s+trigger=/) + expect(block).not.toMatch(/allowPendingToggle\s+animated/) }) it("BashHighlightedOutput syntax highlights the command next to the prompt", () => { @@ -358,7 +359,7 @@ describe("Collapsed deferred tool details contract (source)", () => { it("uses an explicit details hint before touching deferred children", () => { expect(basic).toContain("hasDetails?: boolean") - expect(basic).toContain("props.hasDetails ?? !!props.children") + expect(basic).toContain("props.hasDetails ?? !!hasChildren()") expect(basic).toMatch(/\{props\.children\}<\/Show>/) }) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/migration-cache.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/migration-cache.test.ts new file mode 100644 index 00000000000..a2e4cdd4018 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/migration-cache.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "bun:test" +import { + getMigrationCache, + handleRequestMigrationData, + handleStartMigration, + type MigrationCache, + type MigrationCacheEntry, + type MigrationContext, +} from "../../../src/kilo-provider/handlers/migration" + +const legacy = { + hasData: false, + providers: [], + mcpServers: [], + customModes: [], +} + +function makeContext(cache: MigrationCache): MigrationContext { + return { + client: null, + extensionContext: { globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } }, + postMessage: () => {}, + refreshSessions: () => {}, + migrationCache: cache, + migrationCheckInFlight: false, + disposeGlobal: async () => {}, + broadcastComplete: () => {}, + } as unknown as MigrationContext +} + +describe("migration cache", () => { + it("isolates entries by operation and source", () => { + const cache: MigrationCache = new Map() + const entry: MigrationCacheEntry = { operationId: "new", source: "legacy", data: legacy } + cache.set("new", entry) + + expect(getMigrationCache(cache, "legacy", "new")).toBe(entry) + expect(getMigrationCache(cache, "roo", "new")).toBeUndefined() + expect(getMigrationCache(cache, "legacy", "stale")).toBeUndefined() + }) + + it("retains an empty Roo discovery for its operation", () => { + const cache: MigrationCache = new Map() + const entry: MigrationCacheEntry = { operationId: "empty", source: "roo", data: null } + cache.set("empty", entry) + + expect(getMigrationCache(cache, "roo", "empty")).toBe(entry) + expect(getMigrationCache(cache, "roo", "empty")?.data).toBeNull() + }) + + it("drops entries from abandoned operations when a new request arrives", async () => { + const cache: MigrationCache = new Map() + cache.set("abandoned", { operationId: "abandoned", source: "roo", data: null }) + + await handleRequestMigrationData(makeContext(cache), "roo", "fresh") + + expect(cache.has("abandoned")).toBe(false) + expect(getMigrationCache(cache, "roo", "fresh")).toBeDefined() + }) + + it("evicts an operation's entry once the migration completes", async () => { + const cache: MigrationCache = new Map() + cache.set("op", { operationId: "op", source: "roo", data: null }) + + await handleStartMigration(makeContext(cache), "roo", "op", { + providers: [], + mcpServers: [], + customModes: [], + sessions: [], + defaultModel: false, + settings: { + autoApproval: { + commandRules: false, + readPermission: false, + writePermission: false, + executePermission: false, + mcpPermission: false, + taskPermission: false, + }, + language: false, + autocomplete: false, + }, + }) + + expect(cache.has("op")).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/session-batch.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/session-batch.test.ts new file mode 100644 index 00000000000..dcec9a6eda8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/session-batch.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "bun:test" +import { runSessionBatch } from "../../../src/legacy-migration/session-batch" + +const sessions = [ + { id: "one", title: "One", directory: "/repo", time: 2 }, + { id: "two", title: "Two", directory: "/repo", time: 1 }, +] + +describe("migration session batch", () => { + it("uses consistent progress, skipped, error, and summary semantics", async () => { + const progress: unknown[] = [] + const phases: unknown[] = [] + const results = await runSessionBatch({ + selections: [{ id: "one" }, { id: "two" }], + sessions, + resolve: (id) => ({ id, dir: "/tasks" }), + migrate: async (selection) => + selection.id === "one" ? { ok: true, skipped: true } : { ok: false, message: "broken" }, + onProgress: (...args) => progress.push(args), + onSessionProgress: (value) => phases.push(value), + delay: async () => undefined, + }) + + expect(results).toEqual([ + { item: "One", category: "session", status: "warning", message: "Already imported." }, + { item: "Two", category: "session", status: "error", message: "broken" }, + ]) + expect(progress).toEqual([ + ["one", "migrating"], + ["one", "warning", "Already imported."], + ["two", "migrating"], + ["two", "error", "broken"], + ]) + expect(phases.at(-1)).toMatchObject({ session: sessions[1], index: 2, total: 2, phase: "summary" }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/legacy-migration/task-store.test.ts b/packages/kilo-vscode/tests/unit/legacy-migration/task-store.test.ts new file mode 100644 index 00000000000..6a681eed93f --- /dev/null +++ b/packages/kilo-vscode/tests/unit/legacy-migration/task-store.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it } from "bun:test" +import * as vscode from "vscode" +import { listSessions, resolveSession, scanTaskStore } from "../../../src/legacy-migration/task-store" + +type Fs = typeof vscode.workspace.fs +const fs = vscode.workspace.fs as Fs +const original = { readDirectory: fs.readDirectory, readFile: fs.readFile, stat: fs.stat } +const dir = "/storage/kilocode.kilo-code/tasks" +const api = (id: string) => `${dir}/${id}/api_conversation_history.json` + +describe("task store history scan", () => { + afterEach(() => { + fs.readDirectory = original.readDirectory + fs.readFile = original.readFile + fs.stat = original.stat + }) + + it("includes only history items whose conversation file exists, without scanning or parsing disk", async () => { + let listed = false + let read = false + fs.readDirectory = async () => { + listed = true + return [] + } + fs.readFile = async () => { + read = true + throw new Error("history scan must not read files") + } + fs.stat = async (uri) => { + if (uri.fsPath === api("keep")) return { type: vscode.FileType.File, ctime: 0, mtime: 0, size: 1 } + throw new Error(`missing ${uri.fsPath}`) + } + + const items = [ + { id: "keep", task: "Keep me", workspace: "/repo", ts: 1700000000000 }, + { id: "gone", task: "Deleted on disk", workspace: "/repo", ts: 1700000000001 }, + ] + const scan = await scanTaskStore(dir, items, { mode: "history" }) + + // "gone" is dropped (no file) and on-disk orphans are never considered (no enumeration). + expect(listSessions(scan.catalog)).toEqual([ + { id: "keep", title: "Keep me", directory: "/repo", time: 1700000000000 }, + ]) + expect(resolveSession(scan.catalog, "keep")).toMatchObject({ id: "keep", dir }) + expect(scan.diagnostics).toEqual([]) + expect(listed).toBe(false) + expect(read).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/marketplace-actions.test.ts b/packages/kilo-vscode/tests/unit/marketplace-actions.test.ts index d0f5962110b..f778f9b1f0f 100644 --- a/packages/kilo-vscode/tests/unit/marketplace-actions.test.ts +++ b/packages/kilo-vscode/tests/unit/marketplace-actions.test.ts @@ -7,6 +7,8 @@ import { type MarketplaceRemoveContext, } from "../../src/services/marketplace/actions" import type { McpMarketplaceItem } from "../../src/services/marketplace/types" +import { filterItems, installedScopes, retain } from "../../webview-ui/src/components/marketplace/utils" +import type { MarketplaceItem } from "../../webview-ui/src/types/marketplace" const project = "/repo" const storage = vscode.Uri.file("/storage") @@ -18,6 +20,7 @@ const item: McpMarketplaceItem = { type: "mcp", name: "Memory", description: "", + category: "development", url: "", content: "", } @@ -62,6 +65,71 @@ afterEach(() => { fs.writeFile = original.writeFile }) +describe("Marketplace installation metadata", () => { + it("tracks colliding IDs independently by item type", () => { + const metadata = { + project: { + "mcp:dbt": { type: "mcp" }, + "skill:dbt": { type: "skill" }, + }, + global: {}, + } + + expect(installedScopes("dbt", "mcp", metadata)).toEqual(["project"]) + expect(installedScopes("dbt", "skill", metadata)).toEqual(["project"]) + expect(installedScopes("dbt", "agent", metadata)).toEqual([]) + }) + + it("removes filters that are no longer available", () => { + expect(retain(["agent", "mcp"], ["mcp", "skill"])).toEqual(["mcp"]) + }) + + it("filters the mixed list by search, category, and status", () => { + const items: MarketplaceItem[] = [ + { + type: "agent", + id: "reviewer", + name: "Code Reviewer", + description: "Reviews code", + category: "development", + content: { mode: "all", description: "Reviews code", prompt: "Review" }, + }, + { + type: "mcp", + id: "warehouse", + name: "Warehouse", + description: "Queries data", + category: "web-automation", + url: "https://example.com", + content: "{}", + }, + { + type: "skill", + id: "campaign-writer", + name: "Campaign Writer", + displayName: "Campaign Writer", + description: "Writes campaigns", + category: "business", + displayCategory: "Business", + githubUrl: "https://example.com", + content: "https://example.com/skill.tar.gz", + }, + ] + const metadata = { project: { "mcp:warehouse": { type: "mcp" } }, global: {} } + + expect(filterItems(items, metadata, "reviewer", "all", [], []).map((item) => item.id)).toEqual(["reviewer"]) + expect(filterItems(items, metadata, "web automation", "all", [], []).map((item) => item.id)).toEqual(["warehouse"]) + expect( + filterItems(items, metadata, "servidor mcp", "all", [], [], { mcp: "Servidor MCP" }).map((item) => item.id), + ).toEqual(["warehouse"]) + expect(filterItems(items, metadata, "", "all", ["business"], []).map((item) => item.id)).toEqual([ + "campaign-writer", + ]) + expect(filterItems(items, metadata, "", "installed", [], []).map((item) => item.id)).toEqual(["warehouse"]) + expect(filterItems(items, metadata, "", "all", [], ["mcp"]).map((item) => item.id)).toEqual(["warehouse"]) + }) +}) + describe("Marketplace legacy MCP cleanup", () => { it("preserves global legacy config during project removal", async () => { const files = setup() diff --git a/packages/kilo-vscode/tests/unit/marketplace-installer.test.ts b/packages/kilo-vscode/tests/unit/marketplace-installer.test.ts index 1991576cece..a576f64d95c 100644 --- a/packages/kilo-vscode/tests/unit/marketplace-installer.test.ts +++ b/packages/kilo-vscode/tests/unit/marketplace-installer.test.ts @@ -55,6 +55,7 @@ describe("MarketplaceInstaller MCP format normalization", () => { id: "memory", name: "Memory", description: "test", + category: "development", url: "https://example.com", content: JSON.stringify({ command: "npx", @@ -81,6 +82,7 @@ describe("MarketplaceInstaller MCP format normalization", () => { id: "myremote", name: "Remote", description: "test", + category: "development", url: "https://example.com", content: JSON.stringify({ type: "sse", @@ -105,6 +107,7 @@ describe("MarketplaceInstaller MCP format normalization", () => { id: "already", name: "Already Done", description: "test", + category: "development", url: "https://example.com", content: JSON.stringify({ type: "local", @@ -153,6 +156,7 @@ describe("MarketplaceInstaller skills", () => { id: "test-mcp", name: "Test MCP", description: "test", + category: "development", url: "https://example.com", content: "{}", }, @@ -164,6 +168,7 @@ describe("MarketplaceInstaller skills", () => { id: "test-agent", name: "Test Agent", description: "test", + category: "development", content: { mode: "all", description: "test", prompt: "test" }, }, "project", diff --git a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts index 788a70b61e5..529e8b55658 100644 --- a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts +++ b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts @@ -22,13 +22,32 @@ describe("model preview data collection line", () => { expect(styles).toContain(".model-preview-data-line") }) - it("renders prompt training independently from the free badge", () => { - expect(selector).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) - expect(preview).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) + it("renders prompt training independently from the model badges", () => { + expect(selector).toContain("isDataCollectedModel(model)") + expect(preview).toContain("isDataCollectedModel(model())") + expect(agent).toContain("isDataCollectedModel(model)") + }) + + it("renders BYOK availability independently from training metadata", () => { + expect(selector).toContain("hasByok(model)") + expect(preview).toContain("hasByok(model())") + expect(agent).toContain("hasByok(model)") + expect(selector).toContain(">BYOK") + expect(preview).toContain(">BYOK") + expect(agent).toContain(">BYOK") + }) + + it("shows BYOK instead of Free when both metadata fields are set", () => { + expect(selector).toContain("isFree(model) && !hasByok(model)") + expect(preview).toContain("model().isFree && !hasByok(model())") + expect(agent).toContain("model.isFree && !hasByok(model)") + }) + + it("uses neutral colors for the main model picker BYOK badge", () => { + expect(selector).toContain("model-selector-data-badge--byok") + expect(styles).toContain(".model-selector-data-badge--byok") + expect(styles).toContain("background: var(--vscode-badge-background) !important") + expect(styles).toContain("color: var(--vscode-badge-foreground) !important") }) it("uses the book open check icon for all webview model data disclosures", () => { diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts index 4483a554888..8fbbd5e51c4 100644 --- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts @@ -8,6 +8,7 @@ import { PROVIDER_ORDER, freeDataLabel, isDataCollectedModel, + hasByok, isFree, } from "../../webview-ui/src/components/shared/model-selector-utils" @@ -119,6 +120,14 @@ describe("isDataCollectedModel", () => { }) }) +describe("hasByok", () => { + it("uses only explicit user BYOK metadata", () => { + expect(hasByok({ hasUserByokAvailable: true })).toBe(true) + expect(hasByok({ hasUserByokAvailable: false })).toBe(false) + expect(hasByok({})).toBe(false) + }) +}) + describe("buildTriggerLabel", () => { it("returns resolved model name for non-kilo provider unchanged", () => { expect(buildTriggerLabel("GPT-4o", "openai", undefined, null, false, "", true, labels)).toBe("GPT-4o") diff --git a/packages/kilo-vscode/tests/unit/notebook-context.test.ts b/packages/kilo-vscode/tests/unit/notebook-context.test.ts new file mode 100644 index 00000000000..c4f2742936c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/notebook-context.test.ts @@ -0,0 +1,188 @@ +import { beforeEach, describe, expect, it } from "bun:test" +import * as vscode from "vscode" +import { + autocompleteScope, + getNotebookContext, + notebookUri, + supportsNotebook, +} from "../../src/services/autocomplete/continuedev/core/autocomplete/notebook" +import { accessible } from "../../src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider" +import type { FileIgnoreController } from "../../src/services/autocomplete/shims/FileIgnoreController" + +function uri(scheme: string, path: string, fragment = ""): vscode.Uri { + const value = `${scheme}:${path}${fragment ? `#${fragment}` : ""}` + return { + scheme, + fsPath: path, + toString: () => value, + } as vscode.Uri +} + +function document(id: string, text: string, languageId = "python", version = 1): vscode.TextDocument { + return { + uri: uri("vscode-notebook-cell", "/workspace/example.ipynb", id), + fileName: `/workspace/${id}.py`, + languageId, + version, + getText: () => text, + } as vscode.TextDocument +} + +function notebooks(value: vscode.NotebookDocument[]): void { + Object.defineProperty(vscode.workspace, "notebookDocuments", { + configurable: true, + value, + }) +} + +describe("notebook context", () => { + beforeEach(() => notebooks([])) + + it("flattens notebook cells and translates the cursor", () => { + const markdown = document("markdown", "# Title\nNotes") + const code = document("code", "const value = 1\nvalue += 1", "javascript") + const current = document("current", "print(value)\nprint('done')") + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + getCells: () => [ + { kind: vscode.NotebookCellKind.Markup, document: markdown }, + { kind: vscode.NotebookCellKind.Code, document: code }, + { kind: vscode.NotebookCellKind.Code, document: current }, + ], + } as vscode.NotebookDocument + notebooks([notebook]) + + const context = getNotebookContext(current, new vscode.Position(1, 5)) + + expect(context).toEqual({ + contents: `"""# Title\nNotes"""\n\nconst value = 1\nvalue += 1\n\nprint(value)\nprint('done')`, + filepath: "/workspace/example.ipynb", + position: new vscode.Position(7, 5), + }) + }) + + it("limits notebook completion to Python code cells", () => { + const python = document("python", "value = 1") + const javascript = document("javascript", "const value = 1", "javascript") + const markdown = document("markdown", "# Heading", "markdown") + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + getCells: () => [ + { kind: vscode.NotebookCellKind.Code, document: python }, + { kind: vscode.NotebookCellKind.Code, document: javascript }, + { kind: vscode.NotebookCellKind.Markup, document: markdown }, + ], + } as vscode.NotebookDocument + notebooks([notebook]) + + expect(supportsNotebook(python)).toBe(true) + expect(supportsNotebook(javascript)).toBe(false) + expect(supportsNotebook(markdown)).toBe(false) + expect(getNotebookContext(javascript, new vscode.Position(0, 0))).toBeUndefined() + expect(getNotebookContext(markdown, new vscode.Position(0, 0))).toBeUndefined() + expect(supportsNotebook({ uri: uri("file", "/workspace/file.ts") } as vscode.TextDocument)).toBe(true) + }) + + it("resolves file and notebook cell URIs", () => { + const file = uri("file", "/workspace/file.ts") + const cell = document("code", "value = 1") + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + getCells: () => [{ kind: vscode.NotebookCellKind.Code, document: cell }], + } as vscode.NotebookDocument + notebooks([notebook]) + + expect(notebookUri(file)).toBe(file) + expect(notebookUri(cell.uri)).toBe(notebook.uri) + expect(notebookUri(uri("untitled", "Untitled-1"))).toBeUndefined() + }) + + it("scopes autocomplete cache to the cell and sibling versions", () => { + const current = document("current", "value = 1") + const sibling = document("sibling", "other = 1") + const cells = [ + { kind: vscode.NotebookCellKind.Code, document: current }, + { kind: vscode.NotebookCellKind.Code, document: sibling }, + ] as vscode.NotebookCell[] + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + version: 1, + getCells: () => cells, + } as vscode.NotebookDocument + notebooks([notebook]) + + const initial = autocompleteScope(current) + Object.assign(current, { version: 2 }) + Object.assign(notebook, { version: 2 }) + expect(autocompleteScope(current)).toBe(initial) + + Object.assign(sibling, { version: 2 }) + Object.assign(notebook, { version: 3 }) + expect(autocompleteScope(current)).not.toBe(initial) + expect(autocompleteScope(current)).not.toBe(autocompleteScope(sibling)) + }) + + it("changes autocomplete scope when sibling order changes", () => { + const current = document("current", "value = 1") + const first = document("first", "first = 1") + const second = document("second", "second = 1") + const cells = [ + { kind: vscode.NotebookCellKind.Code, document: first }, + { kind: vscode.NotebookCellKind.Code, document: second }, + { kind: vscode.NotebookCellKind.Code, document: current }, + ] as vscode.NotebookCell[] + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + version: 1, + getCells: () => cells, + } as vscode.NotebookDocument + notebooks([notebook]) + + const initial = autocompleteScope(current) + cells.splice(0, 2, cells[1]!, cells[0]!) + Object.assign(notebook, { version: 2 }) + + expect(autocompleteScope(current)).not.toBe(initial) + }) + + it("reuses notebook resolution within the same notebook version", () => { + const current = document("current", "value = 1") + const cells = [{ kind: vscode.NotebookCellKind.Code, document: current }] as vscode.NotebookCell[] + let calls = 0 + const notebook = { + uri: uri("file", "/workspace/example.ipynb"), + version: 1, + getCells: () => { + calls++ + return cells + }, + } as vscode.NotebookDocument + notebooks([notebook]) + + expect(supportsNotebook(current)).toBe(true) + expect(notebookUri(current.uri)).toBe(notebook.uri) + expect(getNotebookContext(current, new vscode.Position(0, 0))).toBeDefined() + expect(calls).toBe(1) + }) + + it("validates notebook parent paths regardless of URI scheme", () => { + for (const scheme of ["file", "untitled", "memfs"]) { + const cell = document(scheme, "value = 1") + const notebook = { + uri: uri(scheme, `/workspace/${scheme}.ipynb`), + getCells: () => [{ kind: vscode.NotebookCellKind.Code, document: cell }], + } as vscode.NotebookDocument + const paths: string[] = [] + const controller = { + validateAccess: (path: string) => { + paths.push(path) + return false + }, + } as FileIgnoreController + notebooks([notebook]) + + expect(accessible(controller, cell)).toBe(false) + expect(paths).toEqual([`/workspace/${scheme}.ipynb`]) + } + }) +}) diff --git a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts index d5f9dc532de..319f88995fc 100644 --- a/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-drafts.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "bun:test" -import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../webview-ui/src/utils/prompt-drafts" +import { + createdDraftKey, + movePromptDraft, + pendingDraftKey, + scopeDraftKey, + sessionDraftKey, +} from "../../webview-ui/src/utils/prompt-drafts" describe("sessionDraftKey", () => { it("prefixes session ids", () => { @@ -30,3 +36,41 @@ describe("scopeDraftKey", () => { expect(scopeDraftKey("prompt:1")).toBe("prompt:1:empty") }) }) + +describe("createdDraftKey", () => { + it("uses the pending key when a draft id exists", () => { + expect(createdDraftKey("draft-1", true)).toBe("pending:draft-1") + }) + + it("uses the new-chat key for sandbox-triggered session creation", () => { + expect(createdDraftKey(undefined, true)).toBe("new") + }) + + it("ignores unrelated session creation without a draft id", () => { + expect(createdDraftKey()).toBeUndefined() + }) +}) + +describe("movePromptDraft", () => { + it("moves text, review comments, and images to the created session", () => { + const source = scopeDraftKey("prompt:default", createdDraftKey(undefined, true)) + const target = scopeDraftKey("prompt:default", sessionDraftKey("session-1")) + const comment = { id: "comment-1", body: "Keep this review note" } + const image = { id: "image-1", dataUrl: "data:image/png;base64,abc" } + const text = new Map([[source, "Keep this prompt"]]) + const comments = new Map([[source, [comment]]]) + const images = new Map([[source, [image]]]) + + expect(movePromptDraft({ text, comments, images }, source, target)).toEqual({ + text: "Keep this prompt", + comments: [comment], + images: [image], + }) + expect(text.get(target)).toBe("Keep this prompt") + expect(comments.get(target)).toEqual([comment]) + expect(images.get(target)).toEqual([image]) + expect(text.has(source)).toBe(false) + expect(comments.has(source)).toBe(false) + expect(images.has(source)).toBe(false) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts index 40b434fb17c..4fab1cf5a3d 100644 --- a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it } from "bun:test" import { readFileSync } from "node:fs" import { join } from "node:path" -describe("PromptInput connection guard", () => { - const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "PromptInput.tsx") - const src = readFileSync(path, "utf8") +const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "PromptInput.tsx") +const src = readFileSync(path, "utf8") +describe("PromptInput connection guard", () => { it("rechecks the connection after resolving async attachments and before clearing the draft", () => { const attachments = src.indexOf("const gitFile = await git.resolveAttachment") const guard = src.indexOf("if (isDisabled()) return", attachments) @@ -18,3 +18,53 @@ describe("PromptInput connection guard", () => { expect(clear).toBeGreaterThan(send) }) }) + +describe("PromptInput sandbox toggle", () => { + it("toggles or creates the runtime session instead of writing config", () => { + const start = src.indexOf("const toggleSandbox = () =>") + const end = src.indexOf("let enhanceCounter", start) + const toggle = src.slice(start, end) + + expect(start).toBeGreaterThan(-1) + expect(end).toBeGreaterThan(start) + expect(toggle).toContain("const sessionID = sandboxID()") + expect(toggle).toContain("if (!sessionID) saveDraft(draftKey(), text(), reviewComments(), imageAttach.images())") + expect(toggle).toContain('type: "toggleSandbox"') + expect(toggle).toContain("sessionID,") + expect(toggle).toContain("draftID: props.pendingSessionID ?? session.draftSessionID()") + expect(toggle).toContain("requestID,") + expect(toggle).toContain("setSandboxTarget(sessionID ?? null)") + expect(toggle).not.toContain('type: "updateConfig"') + }) + + it("captures edits made while sandbox session creation is pending", () => { + const start = src.indexOf('if (message.type === "sessionCreated")') + const end = src.indexOf('if (message.type === "action"', start) + const created = src.slice(start, end) + const save = created.indexOf( + "if (source === draftKey()) saveDraft(source, text(), reviewComments(), imageAttach.images())", + ) + const move = created.indexOf("movePromptDraft(") + + expect(start).toBeGreaterThan(-1) + expect(end).toBeGreaterThan(start) + expect(save).toBeGreaterThan(-1) + expect(move).toBeGreaterThan(save) + }) + + it("uses the internal flag for visibility and effective runtime state for the button", () => { + expect(src).toContain("features().sandboxControls") + expect(src).toContain("") + expect(src).toContain('message.type === "sandboxStatus"') + expect(src).toContain("message.sessionID !== sandboxID() && !matching") + expect(src).toContain("setSandboxState(state)") + expect(src).toContain("message.requestID === sandboxRequest()") + expect(src).toContain("const target = untrack(sandboxTarget)") + expect(src).toContain("if (target && target !== sessionID) clearSandboxRequest()") + expect(src).toContain("sandbox()?.enabled ?? (!sandboxID() && config().experimental?.sandbox === true)") + expect(src).toContain("aria-pressed={sandboxEnabled()}") + expect(src).toContain("!sandboxReady()") + expect(src).toContain("if (sandboxRequest() && target === null) return") + expect(src).not.toContain("if (state === current) return true") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/prompt-input-utils.test.ts b/packages/kilo-vscode/tests/unit/prompt-input-utils.test.ts index f38a45a1793..258e14f4b27 100644 --- a/packages/kilo-vscode/tests/unit/prompt-input-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-input-utils.test.ts @@ -10,8 +10,41 @@ import { isSuggesting, isQuestioning, isPathMention, + applySandboxState, } from "../../webview-ui/src/components/chat/prompt-input-utils" +describe("applySandboxState", () => { + const state = (enabled: boolean, revision: number, sessionID = "ses_1", directory = "/repo") => ({ + sessionID, + directory, + enabled, + available: true, + version: enabled ? 1 : 0, + revision, + }) + + it("ignores an HTTP response with an older backend version", () => { + const latest = { ...state(true, 1), version: 2 } + const stale = { ...state(false, 2), version: 1 } + expect(applySandboxState(latest, stale)).toEqual(latest) + }) + + it("uses provider revision to order equal backend versions", () => { + const current = { ...state(false, 2), version: 4 } + const older = { ...state(true, 1), version: 4 } + const newer = { ...state(true, 3), version: 4 } + expect(applySandboxState(current, older)).toEqual(current) + expect(applySandboxState(current, newer)).toEqual(newer) + }) + + it("keeps global provider ordering across sessions and directories", () => { + expect(applySandboxState(state(true, 5, "ses_1"), state(false, 1, "ses_2"))).toEqual(state(true, 5, "ses_1")) + expect(applySandboxState(state(true, 5), state(false, 6, "ses_1", "/worktree"))).toEqual( + state(false, 6, "ses_1", "/worktree"), + ) + }) +}) + describe("fileName", () => { it("extracts the last segment of a unix path", () => { expect(fileName("src/components/chat/PromptInput.tsx")).toBe("PromptInput.tsx") diff --git a/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts b/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts index 5be7489e9c7..60b5aa54255 100644 --- a/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts +++ b/packages/kilo-vscode/tests/unit/revert-checkpoints.test.ts @@ -40,16 +40,14 @@ describe("revert session synchronization", () => { expect(unrevert).toContain('type: "sessionUpdated"') }) - it("distinguishes partial sync patches from full bus snapshots", () => { + it("uses ordered sync patches instead of duplicate bus snapshots", () => { expect(provider).toMatch(/source: "sync"/) expect(provider).toMatch( /if \(event\.type === "session\.updated"\) return "source" in event && event\.source === "sync"/, ) + expect(provider).toMatch(/if \(isFullSessionUpdatedEvent\(event\)\) return/) expect(provider).toMatch( - /isLegacySyncEvent\(event\)\s*\? applySessionPatch\(this\.currentSession, event\.properties\.info\)\s*:\s*event\.properties\.info/, - ) - expect(provider).toMatch( - /isFullSessionUpdatedEvent\(event\)\s*\? \{ type: "sessionUpdated" as const, session: this\.sessionToWebview\(event\.properties\.info\) \}/, + /this\.setCurrentSession\(applySessionPatch\(this\.currentSession, event\.properties\.info\)\)/, ) }) }) diff --git a/packages/kilo-vscode/tests/unit/roo-import.test.ts b/packages/kilo-vscode/tests/unit/roo-import.test.ts new file mode 100644 index 00000000000..4b6198ec1a8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/roo-import.test.ts @@ -0,0 +1,172 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import * as vscode from "vscode" +import { createSessionID } from "../../src/legacy-migration/sessions/lib/ids" +import { parseSession } from "../../src/legacy-migration/sessions/parser" +import { detectRooCodeSessions } from "../../src/roo-import/service" + +const enc = new TextEncoder() +const first = "/storage/roovscode.roo-cline/tasks" +const second = "/storage/rooveterinaryinc.roo-cline/tasks" +const customRoot = "/custom/roo/tasks" +const id = "1781613537275" +const other = "1781613537276" +const missing = "1781613537277" +const customId = "1781613537278" +const special = "__proto__" + +type Fs = typeof vscode.workspace.fs +const fs = vscode.workspace.fs as Fs +const original = { + readDirectory: fs.readDirectory, + readFile: fs.readFile, + stat: fs.stat, +} + +let files: Map +let dirs: Map + +describe("roo import", () => { + beforeEach(() => { + files = new Map([ + [ + `${first}/${id}/api_conversation_history.json`, + { value: JSON.stringify([{ role: "user", content: "First root" }]), mtime: 10 }, + ], + [ + `${first}/${special}/api_conversation_history.json`, + { value: JSON.stringify([{ role: "user", content: "Special" }]), mtime: 10 }, + ], + [ + `${first}/${id}/history_item.json`, + { + value: JSON.stringify({ id, ts: Number(id), task: "First root", workspace: "/old", mode: "architect" }), + mtime: 10, + }, + ], + [ + `${first}/_index.json`, + { + value: JSON.stringify({ + version: 1, + entries: [{ id: special, ts: 1, task: "Special", workspace: "/indexed" }], + }), + mtime: 10, + }, + ], + [ + `${second}/${id}/api_conversation_history.json`, + { value: JSON.stringify([{ role: "user", content: "New root" }]), mtime: 20 }, + ], + [ + `${second}/${id}/history_item.json`, + { value: JSON.stringify({ id, ts: Number(id) + 1, task: "New root", workspace: "/new" }), mtime: 20 }, + ], + [ + `${second}/_index.json`, + { + value: JSON.stringify({ + version: 1, + entries: [{ id, ts: Number(id), task: "Stale index", workspace: "/stale" }], + }), + mtime: 10, + }, + ], + [`${second}/${other}/ui_messages.json`, { value: JSON.stringify([{ type: "say", text: "UI only" }]), mtime: 20 }], + [`${second}/bad/api_conversation_history.json`, { value: "not json", mtime: 20 }], + [ + `${second}/${missing}/api_conversation_history.json`, + { value: JSON.stringify([{ role: "user", content: "No workspace" }]), mtime: 20 }, + ], + [ + `${customRoot}/${customId}/api_conversation_history.json`, + { value: JSON.stringify([{ role: "user", content: "Custom root" }]), mtime: 30 }, + ], + [ + `${customRoot}/_index.json`, + { + value: JSON.stringify({ + version: 1, + entries: [{ id: customId, ts: Number(customId), task: "Custom root", workspace: "/custom-repo" }], + }), + mtime: 30, + }, + ], + ]) + dirs = new Map([ + [first, [id, special]], + [second, [id, other, "bad", missing]], + [customRoot, [customId]], + ]) + + fs.readDirectory = async (uri) => { + const entries = dirs.get(uri.fsPath) + if (entries) return entries.map((entry) => [entry, vscode.FileType.Directory]) + throw new Error(`missing dir ${uri.fsPath}`) + } + fs.stat = async (uri) => { + const file = files.get(uri.fsPath) + if (file) return { type: vscode.FileType.File, ctime: 0, mtime: file.mtime, size: file.value.length } + throw new Error(`missing file ${uri.fsPath}`) + } + fs.readFile = async (uri) => { + const file = files.get(uri.fsPath) + if (file) return enc.encode(file.value) + throw new Error(`missing file ${uri.fsPath}`) + } + }) + + afterEach(() => { + fs.readDirectory = original.readDirectory + fs.readFile = original.readFile + fs.stat = original.stat + }) + + it("recovers indexed metadata, selects the newest duplicate, and diagnoses unusable tasks", async () => { + const source = await detectRooCodeSessions({ globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never) + + expect(source?.sessions).toEqual([ + { id, title: "New root", directory: "/new", time: Number(id) + 1 }, + { id: special, title: "Special", directory: "/indexed", time: 1 }, + ]) + expect(source?.catalog.get(id)?.source).toMatchObject({ id, dir: second, namespace: "roo", mtime: 20 }) + expect(source?.catalog.get(special)?.source).toMatchObject({ id: special, dir: first, namespace: "roo" }) + expect(source?.diagnostics.map((item) => [item.id, item.reason])).toEqual([ + [other, "ui-only"], + [missing, "missing-workspace"], + ["bad", "malformed"], + ]) + }) + + it("discovers sessions in Roo's configured custom storage path", async () => { + const source = await detectRooCodeSessions( + { globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never, + "/custom/roo", + ) + + expect(source?.sessions).toContainEqual({ + id: customId, + title: "Custom root", + directory: "/custom-repo", + time: Number(customId), + }) + expect(source?.catalog.get(customId)?.source.dir).toBe(customRoot) + }) + + it("namespaces generated Roo IDs without changing the visible session slug", async () => { + const source = await detectRooCodeSessions({ globalStorageUri: { fsPath: "/storage/kilocode.kilo-code" } } as never) + const entry = source?.catalog.get(id) + expect(entry).toBeDefined() + + const payload = await parseSession( + id, + entry!.source.dir, + entry!.source.item, + [{ role: "user", content: "Hi" }], + `roo:${id}`, + ) + + expect(payload.session.id).toBe(createSessionID(`roo:${id}`)) + expect(payload.session.slug).toBe(id) + expect(payload.messages[0].sessionID).toBe(createSessionID(`roo:${id}`)) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts new file mode 100644 index 00000000000..08ce002d0ec --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sandboxing-settings.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "bun:test" +import { visible } from "../../webview-ui/src/components/settings/sandboxing" + +const features = { indexing: false, sandboxControls: false } + +describe("Sandboxing settings visibility", () => { + test("requires both the internal feature flag and sandbox experiment", () => { + expect(visible(features, {})).toBe(false) + expect(visible({ ...features, sandboxControls: true }, {})).toBe(false) + expect(visible(features, { experimental: { sandbox: true } })).toBe(false) + expect(visible({ ...features, sandboxControls: true }, { experimental: { sandbox: false } })).toBe(false) + expect(visible({ ...features, sandboxControls: true }, { experimental: { sandbox: true } })).toBe(true) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts b/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts index 9fb6aab88f1..c574e9fed35 100644 --- a/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts @@ -7,8 +7,11 @@ import { toErrorMessage, } from "../../src/services/cli-backend/server-manager" import { + copyKiloSandboxWorker, + copySandboxResources, copyTreeSitterResources, resolveTreeSitterEnv, + kiloSandboxWorkerForBinary, treeSitterDirForBinary, treeSitterDirForExtension, } from "../../src/services/cli-backend/cli-resources" @@ -67,6 +70,7 @@ describe("cli tree-sitter resources", () => { expect(treeSitterDirForBinary(bin)).toBe(`${root}/bin/tree-sitter`) expect(treeSitterDirForExtension(root)).toBe(`${root}/bin/tree-sitter`) + expect(kiloSandboxWorkerForBinary(bin)).toBe(`${root}/bin/kilo-sandbox-mutation-worker.js`) expect(resolveTreeSitterEnv(root)).toEqual({ KILO_TREE_SITTER_WASM_DIR: `${root}/bin/tree-sitter` }) }) @@ -76,11 +80,29 @@ describe("cli tree-sitter resources", () => { expect(treeSitterDirForBinary(bin)).toBe(String.raw`${root}\bin\tree-sitter`) expect(treeSitterDirForExtension(root)).toBe(String.raw`${root}\bin\tree-sitter`) + expect(kiloSandboxWorkerForBinary(bin)).toBe(String.raw`${root}\bin\kilo-sandbox-mutation-worker.js`) expect(resolveTreeSitterEnv(root)).toEqual({ KILO_TREE_SITTER_WASM_DIR: String.raw`${root}\bin\tree-sitter`, }) }) + it("copies the Kilo sandbox worker with the packaged CLI binary", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-sandbox-worker-")) + try { + const source = path.join(root, "dist", "@kilocode", "cli-darwin-arm64", "bin", "kilo") + const target = path.join(root, "extension", "bin", "kilo") + await fs.mkdir(path.dirname(source), { recursive: true }) + await fs.mkdir(path.dirname(target), { recursive: true }) + await fs.writeFile(kiloSandboxWorkerForBinary(source), "worker") + + await copyKiloSandboxWorker(source, target) + + expect(await fs.readFile(kiloSandboxWorkerForBinary(target), "utf8")).toBe("worker") + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + it("copies runtime and language WASMs with the packaged CLI binary", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-tree-sitter-")) try { @@ -105,6 +127,73 @@ describe("cli tree-sitter resources", () => { await fs.rm(root, { recursive: true, force: true }) } }) + + it("copies the Linux sandbox helper and license resources", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-sandbox-")) + try { + const source = path.join(root, "dist", "bin", "kilo") + const target = path.join(root, "extension", "bin", "kilo") + const helper = path.join(path.dirname(source), "bwrap") + const license = path.join(path.dirname(source), "licenses", "bubblewrap", "COPYING") + const notice = path.join(path.dirname(license), "NOTICE") + + await fs.mkdir(path.dirname(license), { recursive: true }) + await fs.mkdir(path.dirname(target), { recursive: true }) + await fs.writeFile(source, "binary") + await fs.writeFile(target, "binary") + await fs.writeFile(helper, "helper") + await fs.writeFile(license, "LGPL") + await fs.writeFile(notice, "SPDX-License-Identifier: LGPL-2.0-or-later") + + await copySandboxResources(source, target) + + const copied = path.join(path.dirname(target), "bwrap") + expect(await fs.readFile(copied, "utf8")).toBe("helper") + expect((await fs.stat(copied)).mode & 0o111).not.toBe(0) + expect(await fs.readFile(path.join(path.dirname(target), "licenses", "bubblewrap", "COPYING"), "utf8")).toBe( + "LGPL", + ) + expect(await fs.readFile(path.join(path.dirname(target), "licenses", "bubblewrap", "NOTICE"), "utf8")).toBe( + "SPDX-License-Identifier: LGPL-2.0-or-later", + ) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) + + it("removes stale sandbox resources when the source has none", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-sandbox-stale-")) + try { + const source = path.join(root, "dist", "bin", "kilo") + const target = path.join(root, "extension", "bin", "kilo") + const helper = path.join(path.dirname(target), "bwrap") + const license = path.join(path.dirname(target), "licenses", "bubblewrap", "COPYING") + + await fs.mkdir(path.dirname(source), { recursive: true }) + await fs.mkdir(path.dirname(license), { recursive: true }) + await fs.writeFile(source, "binary") + await fs.writeFile(target, "binary") + await fs.writeFile(helper, "stale helper") + await fs.writeFile(license, "stale license") + + await copySandboxResources(source, target) + + expect( + await fs.stat(helper).then( + () => true, + () => false, + ), + ).toBe(false) + expect( + await fs.stat(path.dirname(license)).then( + () => true, + () => false, + ), + ).toBe(false) + } finally { + await fs.rm(root, { recursive: true, force: true }) + } + }) }) describe("toErrorMessage", () => { diff --git a/packages/kilo-vscode/tests/unit/session-queue.test.ts b/packages/kilo-vscode/tests/unit/session-queue.test.ts index 8e8f6934c3a..77f8ae6a292 100644 --- a/packages/kilo-vscode/tests/unit/session-queue.test.ts +++ b/packages/kilo-vscode/tests/unit/session-queue.test.ts @@ -6,8 +6,10 @@ import { queuedUserMessageIDs, stableMessageTurns, visibleMessages, + visibleParts, + type RevertBoundary, } from "../../webview-ui/src/context/session-queue" -import type { Message, SessionStatusInfo } from "../../webview-ui/src/types/messages" +import type { Message, Part, SessionStatusInfo } from "../../webview-ui/src/types/messages" const base = { sessionID: "session", @@ -30,10 +32,12 @@ const assistant = (id: string, parentID: string, opts: Partial = {}): M ...opts, }) -const layout = (messages: Message[], status: SessionStatusInfo, boundary?: string) => { +const part = (id: string, messageID: string): Part => ({ id, messageID, type: "text", text: id }) + +const layout = (messages: Message[], status: SessionStatusInfo, revert?: RevertBoundary) => { const active = activeUserMessageID(messages, status) return partitionTurns( - messageTurns(messages, boundary), + messageTurns(messages, revert), new Set(active ? [active] : []), new Set(queuedUserMessageIDs(messages, status)), ) @@ -292,7 +296,7 @@ describe("partitionTurns", () => { it("does not render an active turn hidden by a revert boundary", () => { const messages = [user("message_1"), assistant("message_2", "message_1", { finish: "stop" }), user("message_3")] - const result = layout(messages, { type: "busy" }, "message_3") + const result = layout(messages, { type: "busy" }, { messageID: "message_3" }) expect(result.virtual.map((turn) => turn.user.id)).toEqual(["message_1"]) expect(result.direct).toEqual([]) @@ -394,7 +398,45 @@ describe("messageTurns", () => { assistant("message_4", "message_3"), ] - expect(messageTurns(messages, "message_3").map((turn) => turn.user.id)).toEqual(["message_1"]) + expect(messageTurns(messages, { messageID: "message_3" }).map((turn) => turn.user.id)).toEqual(["message_1"]) + }) + + it("keeps the part-boundary assistant and hides later provider errors", () => { + const messages = [ + user("message_1"), + assistant("message_2", "message_1"), + assistant("message_3", "message_1", { error: { name: "ProviderError" } }), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns).toHaveLength(1) + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) + + it("applies assistant boundaries by id when messages arrive out of order", () => { + const messages = [ + user("message_1"), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + assistant("message_2", "message_1"), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) +}) + +describe("visibleParts", () => { + it("keeps only parts before the active part boundary", () => { + const parts = [part("part_1", "message_2"), part("part_2", "message_2"), part("part_3", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_2" })).toEqual([parts[0]]) + }) + + it("fails closed when the boundary part is unavailable", () => { + const parts = [part("part_1", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_missing" })).toEqual([]) }) }) @@ -407,7 +449,10 @@ describe("visibleMessages", () => { assistant("message_4", "message_3"), ] - expect(visibleMessages(messages, "message_3").map((msg) => msg.id)).toEqual(["message_1", "message_2"]) + expect(visibleMessages(messages, { messageID: "message_3" }).map((msg) => msg.id)).toEqual([ + "message_1", + "message_2", + ]) }) it("keeps leading partial assistant output", () => { diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index bb08e44b828..3025d3b0cba 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -1,18 +1,53 @@ /** * SessionTerminalManager tests. * - * The class is tightly coupled to VS Code terminal APIs. We use ts-morph - * static analysis to verify structural invariants that protect against - * real regressions — focusing on ordering constraints and cleanup logic - * that are easy to break during refactoring. + * Structural tests use ts-morph to protect ordering and cleanup invariants. + * Command behavior tests exercise the narrow TerminalHost interface directly. */ import { describe, it, expect } from "bun:test" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts") +const COMMAND = "workbench.action.togglePanel" + +type Handler = (...args: unknown[]) => Promise + +function runtime(run: () => Promise) { + let blocked = false + const handlers = new Map() + const host: TerminalHost = { + createTerminal() { + throw new Error("not used") + }, + activeTerminal: () => undefined, + repoPath: () => undefined, + showWarning() {}, + setContext() {}, + onTerminalClosed: () => ({ dispose() {} }), + onActiveTerminalChanged: () => ({ dispose() {} }), + registerCommand(id, handler) { + if (blocked && id === COMMAND) throw new Error(`command '${id}' already exists`) + handlers.set(id, handler) + return { + dispose() { + if (handlers.get(id) === handler) handlers.delete(id) + }, + } + }, + executeCommand() { + blocked = true + return run() + }, + } + const manager = new SessionTerminalManager(() => {}, host) + const handler = handlers.get(COMMAND) + if (!handler) throw new Error(`command '${COMMAND}' was not registered`) + return { manager, handler } +} function getClass() { const project = new Project({ compilerOptions: { allowJs: true } }) @@ -88,8 +123,35 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.showExistingLocal()") }) + it("panel command registration is best effort", () => { + const text = body("tryRegisterCommand") + expect(text).toContain("this.host.registerCommand") + expect(text).toContain("catch (err)") + expect(text).toContain("panel command registration skipped") + }) + it("exposes active terminal state for terminal context routing", () => { const text = body("hasActiveTerminal") expect(text).toContain("this.host.activeTerminal()") }) }) + +describe("SessionTerminalManager command restoration", () => { + it("preserves the original command result when re-registration fails", async () => { + const expected = { status: "complete" } + const state = runtime(async () => expected) + + expect(await state.handler()).toBe(expected) + state.manager.dispose() + }) + + it("preserves the original command error when re-registration fails", async () => { + const expected = new Error("panel command failed") + const state = runtime(async () => { + throw expected + }) + + await expect(state.handler()).rejects.toBe(expected) + state.manager.dispose() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts new file mode 100644 index 00000000000..acb6ccafe66 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "bun:test" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") + +describe("task tool index identity", () => { + it("preserves a rendered child tool row across status updates", () => { + const result = Bun.spawnSync( + [ + "bun", + "--conditions=browser", + "-e", + ` + import { Window } from "happy-dom" + + const window = new Window() + globalThis.window = window + globalThis.document = window.document + globalThis.Node = window.Node + + const { Index, createComponent, createRenderEffect } = await import("solid-js") + const { createStore } = await import("solid-js/store") + const { render } = await import("solid-js/web") + const { reconcileSessionToolParts, upsertSessionToolPart } = await import( + "./webview-ui/src/context/session-utils.ts" + ) + + const running = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "running", input: { filePath: "src/app.ts" }, title: "Reading" }, + } + const completed = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, + } + const root = document.createElement("div") + const [store, setStore] = createStore({ tools: [running] }) + const dispose = render( + () => + createComponent(Index, { + get each() { + return store.tools + }, + children: (item) => { + const row = document.createElement("div") + createRenderEffect(() => { + row.textContent = item().state.status + }) + return row + }, + }), + root, + ) + const row = root.firstElementChild + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcileSessionToolParts(tools)) + if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced") + if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated") + dispose() + `, + ], + { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, + ) + + const output = result.stdout.toString() + result.stderr.toString() + expect(result.exitCode, output).toBe(0) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/timeline-colors.test.ts b/packages/kilo-vscode/tests/unit/timeline-colors.test.ts index bf7842e041b..16d0819d990 100644 --- a/packages/kilo-vscode/tests/unit/timeline-colors.test.ts +++ b/packages/kilo-vscode/tests/unit/timeline-colors.test.ts @@ -105,4 +105,42 @@ describe("timeline labels", () => { it("returns 'Step finish' for step-finish", () => { expect(label(mkStepFinish())).toBe("Step finish") }) + + it("includes model ID for step-finish with model", () => { + expect( + label( + { + ...mkStepFinish(), + model: { providerID: "kilo", modelID: "openai/gpt-5.5" }, + }, + { + id: "m1", + sessionID: "s1", + role: "assistant", + createdAt: "now", + providerID: "kilo", + modelID: "kilo-auto/efficient", + }, + ), + ).toBe("Step finish · openai/gpt-5.5") + }) + + it("keeps step-finish unchanged for non-auto models", () => { + expect( + label( + { + ...mkStepFinish(), + model: { providerID: "kilo", modelID: "openai/gpt-5.5" }, + }, + { + id: "m1", + sessionID: "s1", + role: "assistant", + createdAt: "now", + providerID: "kilo", + modelID: "openai/gpt-5.5", + }, + ), + ).toBe("Step finish") + }) }) diff --git a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts index a34628e3e8d..6889aab495a 100644 --- a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts +++ b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts @@ -84,6 +84,32 @@ describe("transcriptRows", () => { expect(rows.at(-1)).toMatchObject({ type: "error", message: a3, error: a3.error }) }) + it("hides provider errors at and after an assistant part boundary", () => { + const revert = { messageID: "message_2", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1", { error: { name: "ProviderError" } }) + const parts = { message_2: [part("part_1", "message_2"), part("part_2", "message_2")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.map((row) => row.type)).toEqual(["user", "assistant"]) + expect(rows.filter((row) => row.type === "assistant").flatMap((row) => row.parts.map((item) => item.id))).toEqual([ + "part_1", + ]) + expect(rows.some((row) => row.message.id === "message_3")).toBe(false) + }) + + it("keeps provider errors before an assistant part boundary", () => { + const revert = { messageID: "message_3", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1") + const parts = { message_3: [part("part_1", "message_3"), part("part_2", "message_3")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.at(-1)).toMatchObject({ type: "error", message: a1 }) + }) + it("keeps keys stable when older turns are prepended and parts are appended", () => { const u1 = user("u1") const a1 = assistant("a1", "u1") @@ -131,7 +157,7 @@ describe("transcriptRows", () => { }) const a2 = assistant("a2", "u1") const u3 = user("u3") - const turns = messageTurns([u1, a1, u2, a2, u3], "u3") + const turns = messageTurns([u1, a1, u2, a2, u3], { messageID: "u3" }) const rows = transcriptRows(turns, (id) => (id === "u2" ? (u2.parts ?? []) : [])) expect(rows.map((row) => `${row.turn}:${row.message.id}`)).toEqual(["u1:u1", "u1:a1", "u2:u2", "u2:a2"]) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts b/packages/kilo-vscode/tests/visual-regression.spec.ts index 2fa3fc5c4af..7eb8febaf1f 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts @@ -45,6 +45,30 @@ async function disableAnimations(page: Page) { }) } +async function settle(page: Page) { + const frames = () => + page.evaluate( + () => + new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + }), + ) + + await page.evaluate(async () => { + await document.fonts.ready + }) + await frames() + await page.waitForFunction( + () => { + const root = document.querySelector("#storybook-root") + return root && !root.querySelector('pre > code[data-lang]:not([data-lang="mermaid"])') + }, + undefined, + { timeout: 5_000 }, + ) + await frames() +} + // Stories to skip from visual regression (add IDs here if needed) // Spinner animation captures at an indeterminate frame, causing flaky diffs. // Permission dock config-preloaded has non-deterministic toggle rendering. @@ -97,6 +121,7 @@ for (const story of stories) { ) await disableAnimations(page) await page.waitForSelector("#storybook-root *", { state: "attached" }) + await settle(page) const [component, variant] = story.id.split("--") const root = page.locator("#storybook-root") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx index b9d3d0aa868..71b39fed3c1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx @@ -7,6 +7,7 @@ import { useLanguage } from "../src/context/language" import { KILO_GATEWAY_ID, freeDataLabel, + hasByok, isDataCollectedModel, providerSortKey, } from "../src/components/shared/model-selector-utils" @@ -115,9 +116,14 @@ export const MultiModelSelector: Component<{ } /> {model.name} - + - {freeLabel()} + + {freeLabel()} + + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 5606e835c47..303661b74a3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -151,8 +151,9 @@ export const WorktreeItem: Component = (props) => { = (props) => { }) const providerData = createMemo(() => ({ - all: Object.values(prov.providers()) as unknown as any[], + all: new Map(Object.entries(prov.providers())), connected: prov.connected(), default: prov.defaults(), })) @@ -201,6 +201,7 @@ const AppContent: Component = () => { // legacy-migration: state-driven flag independent of currentView to avoid // race conditions with SettingsEditorProvider's navigate messages. const [migrationNeeded, setMigrationNeeded] = createSignal(false) + const [migrationSource, setMigrationSource] = createSignal<"legacy" | "roo">("legacy") const session = useSession() const server = useServer() const vscode = useVSCode() @@ -278,6 +279,7 @@ const AppContent: Component = () => { // legacy-migration: state-driven migration wizard if (message?.type === "migrationState") { console.log("[Kilo New] App: 🔄 migrationState:", message.needed) + setMigrationSource(message.source) setMigrationNeeded(message.needed) } } @@ -338,9 +340,9 @@ const AppContent: Component = () => { { + onMigrationClick={(source) => { + setMigrationSource(source) setMigrationNeeded(true) - vscode.postMessage({ type: "requestLegacyMigrationData" }) }} /> @@ -350,7 +352,11 @@ const AppContent: Component = () => { } > - setMigrationNeeded(false)} onComplete={() => setMigrationNeeded(false)} /> + setMigrationNeeded(false)} + onComplete={() => setMigrationNeeded(false)} + /> {/* legacy-migration end */}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index ed2ce65d7b2..d9e69e6528d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -332,6 +332,7 @@ export const ChatView: Component = (props) => { suggestions={standaloneSuggestions} readonly={props.readonly} emptyState={props.emptyState} + announce={isSidebar()} />
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 19e8f81b969..5913ef4f366 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -63,6 +63,8 @@ interface MessageListProps { readonly?: boolean /** Optionally replace the standard welcome content while the conversation is empty. */ emptyState?: () => JSX.Element + /** Announce transcript changes as a live log. Disable for multi-session surfaces with concurrent streams. */ + announce?: boolean } export const MessageList: Component = (props) => { @@ -73,6 +75,19 @@ export const MessageList: Component = (props) => { const autoScroll = createAutoScroll({ working: () => session.status() !== "idle", }) + const [announcement, setAnnouncement] = createSignal("") + createEffect( + (prev: { sid?: string; working: boolean }) => { + const sid = session.currentSessionID() + const working = session.status() !== "idle" + if (working && (!prev.working || prev.sid !== sid)) setAnnouncement(language.t("session.status.working")) + if (!working && prev.working && prev.sid === sid) { + setAnnouncement(language.t("settings.agentBehaviour.editMode.save")) + } + return { sid, working } + }, + { sid: undefined, working: false }, + ) // Explicit output-producing actions resume auto-scroll before appending. const onResumeAutoScroll = () => autoScroll.resume() @@ -91,14 +106,14 @@ export const MessageList: Component = (props) => { const [virtualizer, setVirtualizer] = createSignal() const [layout, setLayout] = createSignal("") - const boundary = () => session.revert()?.messageID + const revert = () => session.revert() ?? undefined const turns = createMemo((prev: MessageTurn[] | undefined) => stableMessageTurns( - messageTurns(session.messages(), boundary(), (msg) => session.getParts(msg.id)), + messageTurns(session.messages(), revert(), (msg) => session.getParts(msg.id)), prev, ), ) - const isEmpty = () => turns().length === 0 && !session.loading() && !boundary() + const isEmpty = () => turns().length === 0 && !session.loading() && !revert() const activeUserID = createMemo(() => getActiveUserMessageID(session.messages(), session.statusInfo(), (msg) => session.getParts(msg.id)), @@ -115,6 +130,7 @@ export const MessageList: Component = (props) => { queued: queuedIDs(), live: new Set(active ? [active] : []), hidden: session.isErrorHidden, + revert: revert(), }, prev, ) @@ -260,13 +276,25 @@ export const MessageList: Component = (props) => { return (
+ +
+ {announcement()} +
+
-
+
@@ -326,7 +354,7 @@ export const MessageList: Component = (props) => {
- + {(row) => } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 71b87c08ce7..51daa8f9776 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -13,6 +13,7 @@ import { showToast } from "@kilocode/kilo-ui/toast" import { useSession } from "../../context/session" import { useServer } from "../../context/server" import { useIndexing } from "../../context/indexing" +import { indexingButtonVisible } from "../../context/indexing-utils" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" import { useConfig } from "../../context/config" @@ -42,10 +43,18 @@ import { insertSpacedText, isPromptBusy, isPathMention, + applySandboxState, + type SandboxState, } from "./prompt-input-utils" -import type { ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages" +import type { ExtensionMessage, ReviewComment, SendMessageFailedMessage, TextPart } from "../../types/messages" import { formatReviewCommentsMarkdown } from "../../utils/review-comment-markdown" -import { pendingDraftKey, scopeDraftKey, sessionDraftKey } from "../../utils/prompt-drafts" +import { + createdDraftKey, + movePromptDraft, + pendingDraftKey, + scopeDraftKey, + sessionDraftKey, +} from "../../utils/prompt-drafts" import { ReviewComments } from "./ReviewComments" import { partReview, reviewBody } from "../../../../src/shared/review-comments" import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" @@ -78,7 +87,7 @@ export const PromptInput: Component = (props) => { const session = useSession() const server = useServer() const indexing = useIndexing() - const { config, features } = useConfig() + const { config, globalConfig, settings, features } = useConfig() const provider = useProvider() const language = useLanguage() const vscode = useVSCode() @@ -142,9 +151,83 @@ export const PromptInput: Component = (props) => { const [reviewComments, setReviewComments] = createSignal([]) const [enhancing, setEnhancing] = createSignal(false) const [autoApprove, setAutoApprove] = createSignal(false) + const [sandboxState, setSandboxState] = createSignal() + const [sandboxRequest, setSandboxRequest] = createSignal() + const [sandboxTarget, setSandboxTarget] = createSignal() + let sandboxRetry: ReturnType | undefined + let sandboxAttempts = 0 + const sandboxID = () => { + const id = session.currentSessionID() + return id?.startsWith("cloud:") ? undefined : id + } + const sandboxVisible = () => { + const id = session.currentSessionID() + return features().sandboxControls && !id?.startsWith("cloud:") + } + const sandbox = () => { + const state = sandboxState() + return state?.sessionID === sandboxID() ? state : undefined + } + const sandboxEnabled = () => sandbox()?.enabled ?? (!sandboxID() && config().experimental?.sandbox === true) + const sandboxReady = () => !sandboxID() || sandbox() !== undefined + const requestSandbox = () => { + const sessionID = sandboxID() + if (!sessionID || server.connectionState() !== "connected") return + vscode.postMessage({ type: "requestSandboxStatus", sessionID }) + } + const toggleSandbox = () => { + const sessionID = sandboxID() + const state = sandbox() + if ((sessionID && !state) || state?.available === false || sandboxRequest() || !server.isConnected()) return + const requestID = crypto.randomUUID() + if (!sessionID) saveDraft(draftKey(), text(), reviewComments(), imageAttach.images()) + setSandboxRequest(requestID) + setSandboxTarget(sessionID ?? null) + vscode.postMessage({ + type: "toggleSandbox", + sessionID, + draftID: props.pendingSessionID ?? session.draftSessionID(), + requestID, + agentManagerContext: ctx(), + }) + } + const clearSandboxRequest = () => { + setSandboxRequest(undefined) + setSandboxTarget(undefined) + } + const retrySandbox = (sessionID: string) => { + if (sandboxAttempts >= 2) return + sandboxAttempts++ + if (sandboxRetry) clearTimeout(sandboxRetry) + sandboxRetry = setTimeout(() => { + sandboxRetry = undefined + if (sandboxID() === sessionID) requestSandbox() + }, 1000) + } let enhanceCounter = 0 let preEnhanceText: string | null = null + createEffect(() => { + const sessionID = sandboxID() + const connected = server.connectionState() === "connected" + const target = untrack(sandboxTarget) + if (sandboxRetry) clearTimeout(sandboxRetry) + sandboxRetry = undefined + sandboxAttempts = 0 + if (!connected) { + clearSandboxRequest() + setSandboxState(undefined) + return + } + if (target && target !== sessionID) clearSandboxRequest() + if (!sessionID) { + setSandboxState(undefined) + return + } + if (sandboxRequest() && target === null) return + requestSandbox() + }) + const ghost = useGhostText(vscode, text, () => server.isConnected()) const speech = useSpeechToText(vscode, server, language) @@ -295,6 +378,13 @@ export const PromptInput: Component = (props) => { const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.(), session.submitting()) + const showIndexing = () => + indexingButtonVisible( + features().indexing, + Boolean(settings()["indexing.showButtonWhenDisabled"] ?? true), + config(), + globalConfig(), + ) const isDisabled = () => !server.isConnected() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) @@ -369,7 +459,66 @@ export const PromptInput: Component = (props) => { imageDrafts.set(target, images) } + const handleSandboxMessage = (message: ExtensionMessage) => { + if (message.type === "sandboxStatus") { + const matching = message.requestID !== undefined && message.requestID === sandboxRequest() + if (message.sessionID !== sandboxID() && !matching) return false + if (!server.isConnected()) return true + const current = sandboxState() + if (matching) clearSandboxRequest() + const state = applySandboxState(current, message) + if (state !== current) setSandboxState(state) + sandboxAttempts = 0 + if (sandboxRetry) clearTimeout(sandboxRetry) + sandboxRetry = undefined + if (matching && !state.available) { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: state.reason, + }) + } + return true + } + + if (message.type === "sandboxStatusError") { + const matching = message.requestID !== undefined && message.requestID === sandboxRequest() + if (message.sessionID !== sandboxID() && !matching) return false + if (!server.isConnected()) return true + const current = sandboxState() + if (matching) clearSandboxRequest() + if ((current?.revision ?? -1) > message.revision) return true + if (!message.requestID) { + const same = current?.sessionID === message.sessionID && current.directory === message.directory + setSandboxState({ + sessionID: message.sessionID, + directory: message.directory, + enabled: same ? current.enabled : false, + available: false, + reason: message.message, + version: same ? current.version : 0, + revision: message.revision, + }) + retrySandbox(message.sessionID) + } + if (matching) { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: message.message, + }) + } + return true + } + + if (message.type !== "configUpdated") return false + requestSandbox() + return true + } + const unsubscribe = vscode.onMessage((message) => { + if (handleSandboxMessage(message)) return + if (message.type === "setChatBoxMessage") { setText(message.text) mention.seedFromText(message.text) @@ -414,19 +563,19 @@ export const PromptInput: Component = (props) => { restoreFailed(message as SendMessageFailedMessage) } - if (message.type === "sessionCreated" && message.draftID) { - const target = scopeDraftKey(boxKey(), pendingDraftKey(message.draftID) ?? "new") - const next = scopeDraftKey(boxKey(), sessionDraftKey(message.session.id) ?? "new") - const draft = drafts.get(target) - const pending = reviewDrafts.get(target) - const imgs = imageDrafts.get(target) - if (draft !== undefined) drafts.set(next, draft) - if (pending) reviewDrafts.set(next, pending) - if (imgs) imageDrafts.set(next, imgs) - drafts.delete(target) - reviewDrafts.delete(target) - imageDrafts.delete(target) - if (!session.currentSessionID() && (props.pendingSessionID ?? session.draftSessionID()) === message.draftID) { + if (message.type === "sessionCreated") { + const raw = createdDraftKey(message.draftID, sandboxRequest() !== undefined && sandboxTarget() === null) + if (raw) { + const source = scopeDraftKey(boxKey(), raw) + const target = scopeDraftKey(boxKey(), sessionDraftKey(message.session.id)) + if (source === draftKey()) saveDraft(source, text(), reviewComments(), imageAttach.images()) + movePromptDraft({ text: drafts, comments: reviewDrafts, images: imageDrafts }, source, target) + } + if ( + message.draftID && + !session.currentSessionID() && + (props.pendingSessionID ?? session.draftSessionID()) === message.draftID + ) { session.setDraftSessionID(message.session.id) } } @@ -461,6 +610,7 @@ export const PromptInput: Component = (props) => { onCleanup(() => { // Persist current draft before unmounting saveDraft(draftKey(), text(), reviewComments(), imageAttach.images()) + if (sandboxRetry) clearTimeout(sandboxRetry) unsubAutoApprove() unsubscribe() }) @@ -1015,7 +1165,7 @@ export const PromptInput: Component = (props) => {
- + + + + + +
{(text) => } - + {(item) => { - const info = createMemo(() => getToolInfo(item.tool, item.state?.input)) + const info = createMemo(() => getToolInfo(item().tool, item().state?.input)) const subtitle = createMemo(() => { if (info().subtitle) return info().subtitle - const state = item.state as { status: string; title?: string } - if (state.status === "completed" || state.status === "running") { - return state.title - } + const state = item().state as { status: string; title?: string } + if (state.status === "completed" || state.status === "running") return state.title return undefined }) return ( @@ -162,7 +160,7 @@ const TaskToolRenderer: Component = (props) => {
) }} - +
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/prompt-input-utils.ts b/packages/kilo-vscode/webview-ui/src/components/chat/prompt-input-utils.ts index b78b97624c6..6251efee295 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/prompt-input-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/chat/prompt-input-utils.ts @@ -1,3 +1,22 @@ +export type SandboxState = { + sessionID: string + enabled: boolean + available: boolean + reason?: string + version: number + directory: string + revision: number +} + +export function applySandboxState(current: SandboxState | undefined, next: SandboxState) { + if (!current) return next + const same = current.sessionID === next.sessionID && current.directory === next.directory + if (same && current.version > next.version) return current + if (same && current.version === next.version && current.revision > next.revision) return current + if (!same && current.revision > next.revision) return current + return next +} + export function fileName(path: string): string { const normalized = path.replaceAll("\\", "/").replace(/\/+$/, "") return normalized.split("/").pop() ?? normalized diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx index b74e556edeb..bfb4a9aaf8d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx @@ -23,6 +23,11 @@ export const ItemCard = (props: Props) => { const scopes = () => installedScopes(props.item.id, props.item.type, props.metadata) const installed = () => scopes().length > 0 const name = () => props.displayName ?? props.item.name + const type = () => { + if (props.item.type === "mcp") return t("marketplace.badge.mcpServer") + if (props.item.type === "agent") return t("marketplace.remove.type.agent") + return t("marketplace.remove.type.skill") + } const [expanded, setExpanded] = createSignal(false) const [clamped, setClamped] = createSignal(false) let ref: HTMLParagraphElement | undefined @@ -44,6 +49,7 @@ export const ItemCard = (props: Props) => { {name()} + {type()}
diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx index 8bb615fa386..c3fe6136a65 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -1,4 +1,4 @@ -import { createSignal, createMemo, For, Show } from "solid-js" +import { createSignal, createMemo, createEffect, For, Show } from "solid-js" import { TextField } from "@kilocode/kilo-ui/text-field" import { Select } from "@kilocode/kilo-ui/select" import { Tag } from "@kilocode/kilo-ui/tag" @@ -10,7 +10,7 @@ import type { MarketplaceInstalledMetadata, } from "../../types/marketplace" import { useLanguage } from "../../context/language" -import { isInstalled } from "./utils" +import { filterItems, retain } from "./utils" import { ItemCard } from "./ItemCard" import { MarketplaceContribute } from "./MarketplaceContribute" @@ -23,7 +23,6 @@ interface Props { items: MarketplaceItem[] metadata: MarketplaceInstalledMetadata fetching: boolean - type: "mcp" | "agent" | "skill" searchPlaceholder: string emptyMessage: string onInstall: (item: MarketplaceItem) => void @@ -34,7 +33,8 @@ export const MarketplaceListView = (props: Props) => { const { t } = useLanguage() const [search, setSearch] = createSignal("") const [status, setStatus] = createSignal({ value: "all", label: t("marketplace.filter.all") }) - const [tags, setTags] = createSignal([]) + const [types, setTypes] = createSignal([]) + const [categories, setCategories] = createSignal([]) const options = (): StatusOption[] => [ { value: "all", label: t("marketplace.filter.all") }, @@ -42,51 +42,54 @@ export const MarketplaceListView = (props: Props) => { { value: "notInstalled", label: t("marketplace.filter.notInstalled") }, ] - const tagsFor = (item: MarketplaceItem): string[] => { - if (item.type === "skill") return [(item as SkillMarketplaceItem).displayCategory] - return item.tags ?? [] + const label = (category: string) => + category + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + + const allTypes = createMemo(() => { + const available = new Set(props.items.map((item) => item.type)) + return (["agent", "mcp", "skill"] as const).filter((type) => available.has(type)) + }) + const allCategories = createMemo(() => Array.from(new Set(props.items.map((item) => item.category))).sort()) + + const typeLabel = (type: MarketplaceItem["type"]) => { + if (type === "mcp") return t("marketplace.badge.mcpServer") + if (type === "agent") return t("marketplace.remove.type.agent") + return t("marketplace.remove.type.skill") } - const allTags = createMemo(() => { - const counts = new Map() - for (const item of props.items) { - for (const tag of tagsFor(item)) counts.set(tag, (counts.get(tag) ?? 0) + 1) - } - const min = props.type === "mcp" ? 5 : 1 - return Array.from(counts.entries()) - .filter(([, n]) => n >= min) - .map(([tag]) => tag) - .sort() + createEffect(() => { + setTypes((current) => retain(current, allTypes())) + setCategories((current) => retain(current, allCategories())) }) - const toggleTag = (tag: string) => { - const current = tags() - if (current.includes(tag)) { - setTags(current.filter((t) => t !== tag)) - } else { - setTags([...current, tag]) + const toggleType = (type: MarketplaceItem["type"]) => { + const current = types() + if (current.includes(type)) { + setTypes(current.filter((value) => value !== type)) + return } + setTypes([...current, type]) } - const filtered = createMemo(() => { - const q = search().toLowerCase() - const s = status().value - const active = tags() - return props.items.filter((item) => { - if (s === "installed" && !isInstalled(item.id, item.type, props.metadata)) return false - if (s === "notInstalled" && isInstalled(item.id, item.type, props.metadata)) return false - if (active.length > 0 && !active.some((tag) => tagsFor(item).includes(tag))) return false - if (!q) return true - const skill = item.type === "skill" ? (item as SkillMarketplaceItem) : undefined - return ( - item.id.toLowerCase().includes(q) || - item.name.toLowerCase().includes(q) || - item.description.toLowerCase().includes(q) || - (item.author?.toLowerCase().includes(q) ?? false) || - (skill?.displayName.toLowerCase().includes(q) ?? false) - ) - }) - }) + const toggleCategory = (category: string) => { + const current = categories() + if (current.includes(category)) { + setCategories(current.filter((value) => value !== category)) + return + } + setCategories([...current, category]) + } + + const filtered = createMemo(() => + filterItems(props.items, props.metadata, search(), status().value, categories(), types(), { + agent: typeLabel("agent"), + mcp: typeLabel("mcp"), + skill: typeLabel("skill"), + }), + ) return (
@@ -102,16 +105,33 @@ export const MarketplaceListView = (props: Props) => { onSelect={(v: StatusOption | undefined) => v && setStatus(v)} />
- 0}> -
- - {(tag) => ( + 1}> +
+ + {(type) => ( + )} + +
+
+ 0}> +
+ + {(category) => ( + )} @@ -147,7 +167,7 @@ export const MarketplaceListView = (props: Props) => { linkUrl={skill?.githubUrl ?? mcp?.url} onInstall={props.onInstall} onRemove={props.onRemove} - footer={{(tag) => {tag}}} + footer={{label(item.category)}} /> ) }} diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index b8deefc54e3..8892ec1aa11 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -1,18 +1,11 @@ -import { createSignal, createMemo, createEffect, onCleanup, onMount, Show } from "solid-js" -import { Tabs } from "@kilocode/kilo-ui/tabs" +import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" import { Button } from "@kilocode/kilo-ui/button" import { useVSCode } from "../../context/vscode" import { useServer } from "../../context/server" import { useLanguage } from "../../context/language" import { useDialog } from "@kilocode/kilo-ui/context/dialog" -import type { - MarketplaceItem, - McpMarketplaceItem, - AgentMarketplaceItem, - SkillMarketplaceItem, - MarketplaceInstalledMetadata, -} from "../../types/marketplace" +import type { MarketplaceItem, MarketplaceInstalledMetadata } from "../../types/marketplace" import { TelemetryEventName } from "../../../../src/services/telemetry/types" import { MarketplaceListView } from "./MarketplaceListView" import { InstallModal } from "./InstallModal" @@ -31,14 +24,9 @@ export const MarketplaceView = () => { const [metadata, setMetadata] = createSignal(EMPTY_METADATA) const [fetching, setFetching] = createSignal(true) const [errors, setErrors] = createSignal([]) - const [tab, setTab] = createSignal("agent") const [pending, setPending] = createSignal<{ item: MarketplaceItem; scope: "project" | "global" } | null>(null) const [showMigrationBanner, setShowMigrationBanner] = createSignal(false) - const skills = createMemo(() => items().filter((i): i is SkillMarketplaceItem => i.type === "skill")) - const mcps = createMemo(() => items().filter((i): i is McpMarketplaceItem => i.type === "mcp")) - const agents = createMemo(() => items().filter((i): i is AgentMarketplaceItem => i.type === "agent")) - const fetchData = () => { setFetching(true) vscode.postMessage({ type: "fetchMarketplaceData" }) @@ -158,62 +146,23 @@ export const MarketplaceView = () => { ))} - - - {t("marketplace.tab.agents")} - {t("marketplace.tab.mcp")} - {t("marketplace.tab.skills")} - - -
- - - - {t("marketplace.migration.notice")} - - - - - - - - - - - - - -
-
+ + + {t("marketplace.migration.notice")} + + + +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css index b783ef5b59a..e5f8cdcb8b8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css @@ -9,25 +9,12 @@ gap: 8px; } -.marketplace-tabs-root { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; -} - -.marketplace-content { - flex: 1; - overflow: auto; -} - /* List layout */ .marketplace-list { display: flex; flex-direction: column; gap: 12px; - padding-top: 12px; } .marketplace-filters { @@ -40,30 +27,36 @@ flex: 1; } -.marketplace-category-filters { - overflow-x: auto; -} +/* Type and category filters */ -/* Tag filters */ - -.marketplace-active-tags { +.marketplace-types, +.marketplace-categories { display: flex; flex-wrap: wrap; gap: 4px; } -.marketplace-tag-filter { +.marketplace-type-filter, +.marketplace-category-filter { all: unset; cursor: pointer; opacity: 0.5; transition: opacity 0.15s ease; } -.marketplace-tag-filter:hover { +.marketplace-type-filter:hover, +.marketplace-category-filter:hover { opacity: 0.8; } -.marketplace-tag-filter.active { +.marketplace-type-filter:focus-visible, +.marketplace-category-filter:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + +.marketplace-type-filter.active, +.marketplace-category-filter.active { opacity: 1; } @@ -210,6 +203,32 @@ /* Installed badge */ +.marketplace-badge-type { + flex-shrink: 0; + letter-spacing: 0.4px; +} + +.marketplace-type-agent [data-component="tag"], +.marketplace-badge-type.marketplace-type-agent { + color: var(--text-on-success-base) !important; + background-color: color-mix(in srgb, var(--border-success-base) 14%, transparent) !important; + border-color: var(--border-success-base) !important; +} + +.marketplace-type-mcp [data-component="tag"], +.marketplace-badge-type.marketplace-type-mcp { + color: var(--text-on-info-base) !important; + background-color: color-mix(in srgb, var(--border-info-base) 14%, transparent) !important; + border-color: var(--border-info-base) !important; +} + +.marketplace-type-skill [data-component="tag"], +.marketplace-badge-type.marketplace-type-skill { + color: var(--text-on-warning-base) !important; + background-color: color-mix(in srgb, var(--border-warning-base) 14%, transparent) !important; + border-color: var(--border-warning-base) !important; +} + .marketplace-badge-installed { color: var(--text-on-success-base) !important; border-color: var(--border-success-base) !important; @@ -326,11 +345,13 @@ body.vscode-high-contrast-light { border: 1px solid var(--vscode-contrastBorder, transparent); } - .marketplace-tag-filter { + .marketplace-type-filter, + .marketplace-category-filter { border: 1px solid var(--vscode-contrastBorder, transparent); } - .marketplace-tag-filter.active { + .marketplace-type-filter.active, + .marketplace-category-filter.active { border-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); } diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts index 9504e734f37..0f616cc0c42 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts @@ -1,4 +1,10 @@ -import type { MarketplaceInstalledMetadata } from "../../types/marketplace" +import type { MarketplaceInstalledMetadata, MarketplaceItem } from "../../types/marketplace" + +export function retain(selected: T[], available: T[]): T[] { + const values = new Set(available) + const next = selected.filter((value) => values.has(value)) + return next.length === selected.length ? selected : next +} export function isInstalled( id: string, @@ -14,7 +20,44 @@ export function installedScopes( metadata: MarketplaceInstalledMetadata, ): ("project" | "global")[] { const scopes: ("project" | "global")[] = [] - if (metadata.project[id]?.type === type) scopes.push("project") - if (metadata.global[id]?.type === type) scopes.push("global") + const key = `${type}:${id}` + if (metadata.project[key]?.type === type) scopes.push("project") + if (metadata.global[key]?.type === type) scopes.push("global") return scopes } + +function matches(item: MarketplaceItem, query: string, labels: Partial>) { + const skill = item.type === "skill" ? item : undefined + return ( + item.id.toLowerCase().includes(query) || + item.name.toLowerCase().includes(query) || + item.description.toLowerCase().includes(query) || + item.category.replaceAll("-", " ").toLowerCase().includes(query) || + item.type.includes(query) || + (labels[item.type]?.toLowerCase().includes(query) ?? false) || + (item.author?.toLowerCase().includes(query) ?? false) || + (skill?.displayName.toLowerCase().includes(query) ?? false) + ) +} + +export function filterItems( + items: MarketplaceItem[], + metadata: MarketplaceInstalledMetadata, + search: string, + status: string, + categories: string[], + types: MarketplaceItem["type"][], + labels: Partial> = {}, +): MarketplaceItem[] { + const query = search.trim().toLowerCase() + return items + .filter((item) => { + if (status === "installed" && !isInstalled(item.id, item.type, metadata)) return false + if (status === "notInstalled" && isInstalled(item.id, item.type, metadata)) return false + if (types.length > 0 && !types.includes(item.type)) return false + if (categories.length > 0 && !categories.includes(item.category)) return false + if (!query) return true + return matches(item, query, labels) + }) + .sort((a, b) => a.name.localeCompare(b.name)) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx index af0cea4e6e9..fb749c34d98 100644 --- a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx @@ -30,10 +30,11 @@ import type { MigrationResultItem, MigrationAutoApprovalSelections, LegacySettings, - LegacyMigrationDataMessage, - LegacyMigrationProgressMessage, - LegacyMigrationSessionProgressMessage, - LegacyMigrationCompleteMessage, + MigrationDataMessage, + MigrationProgressMessage, + MigrationSessionProgressMessage, + MigrationCompleteMessage, + MigrationSource, } from "../../types/messages" import "./migration.css" @@ -182,16 +183,36 @@ interface ProgressEntry { // --------------------------------------------------------------------------- export interface MigrationWizardProps { + source?: MigrationSource + operationId?: string onBack: () => void onComplete: () => void } +const capabilities = { + legacy: { + intro: true, + persist: true, + cleanup: true, + categories: new Set(["providers", "mcpServers", "customModes", "sessions", "defaultModel", "settings"]), + }, + roo: { + intro: false, + persist: false, + cleanup: false, + categories: new Set(["sessions"]), + }, +} satisfies Record }> + const MigrationWizard: Component = (props) => { const vscode = useVSCode() const dialog = useDialog() const language = useLanguage() + const source = props.source ?? "legacy" + const config = capabilities[source] + const operationId = props.operationId ?? crypto.randomUUID() - const [screen, setScreen] = createSignal("whats-new") + const [screen, setScreen] = createSignal(config.intro ? "whats-new" : "migrate") const [phase, setPhase] = createSignal("selecting") // Data from extension @@ -226,46 +247,49 @@ const MigrationWizard: Component = (props) => { // Message handling // --------------------------------------------------------------------------- + const applyData = (data: MigrationDataMessage["data"]) => { + setProviders(config.categories.has("providers") ? data.providers : []) + setMcpServers(config.categories.has("mcpServers") ? data.mcpServers : []) + setCustomModes(config.categories.has("customModes") ? data.customModes : []) + setSessions(config.categories.has("sessions") ? (data.sessions ?? []) : []) + setDefaultModel(config.categories.has("defaultModel") ? data.defaultModel : undefined) + setLegacySettings(config.categories.has("settings") ? data.settings : undefined) + setMigrateProviders( + config.categories.has("providers") && data.providers.some((item) => item.supported && item.hasApiKey), + ) + setMigrateMcpServers(config.categories.has("mcpServers") && data.mcpServers.length > 0) + setMigrateModes(config.categories.has("customModes") && data.customModes.length > 0) + setMigrateSessions(config.categories.has("sessions") && (data.sessions?.length ?? 0) > 0) + setMigrateDefaultModel(config.categories.has("defaultModel") && Boolean(data.defaultModel)) + + const settings = config.categories.has("settings") ? data.settings : undefined + if (!settings) return + setMigrateAutoApproval( + settings.autoApprovalEnabled !== undefined || + Boolean(settings.allowedCommands?.length) || + Boolean(settings.deniedCommands?.length) || + settings.alwaysAllowReadOnly !== undefined || + settings.alwaysAllowReadOnlyOutsideWorkspace !== undefined || + settings.alwaysAllowWrite !== undefined || + settings.alwaysAllowExecute !== undefined || + settings.alwaysAllowMcp !== undefined || + settings.alwaysAllowModeSwitch !== undefined || + settings.alwaysAllowSubtasks !== undefined, + ) + setMigrateLanguage(Boolean(settings.language)) + setMigrateAutocomplete(Boolean(settings.autocomplete)) + } + onMount(() => { const handler = (event: MessageEvent) => { const msg = event.data - if (msg?.type === "legacyMigrationData") { - const data = (msg as LegacyMigrationDataMessage).data - setProviders(data.providers) - setMcpServers(data.mcpServers) - setCustomModes(data.customModes) - setSessions(data.sessions ?? []) - setDefaultModel(data.defaultModel) - setLegacySettings(data.settings) - - // Pre-select groups that have data - setMigrateProviders(data.providers.some((p) => p.supported && p.hasApiKey)) - setMigrateMcpServers(data.mcpServers.length > 0) - setMigrateModes(data.customModes.length > 0) - setMigrateSessions((data.sessions?.length ?? 0) > 0) - setMigrateDefaultModel(Boolean(data.defaultModel)) - - const s = data.settings - if (s) { - setMigrateAutoApproval( - s.autoApprovalEnabled !== undefined || - Boolean(s.allowedCommands?.length) || - Boolean(s.deniedCommands?.length) || - s.alwaysAllowReadOnly !== undefined || - s.alwaysAllowReadOnlyOutsideWorkspace !== undefined || - s.alwaysAllowWrite !== undefined || - s.alwaysAllowExecute !== undefined || - s.alwaysAllowMcp !== undefined || - s.alwaysAllowModeSwitch !== undefined || - s.alwaysAllowSubtasks !== undefined, - ) - setMigrateLanguage(Boolean(s.language)) - setMigrateAutocomplete(Boolean(s.autocomplete)) - } + if (msg?.source !== source || msg?.operationId !== operationId) return + if (msg.type === "migrationData") { + applyData((msg as MigrationDataMessage).data) } - if (msg?.type === "legacyMigrationProgress") { - const update = msg as LegacyMigrationProgressMessage + if (msg.type === "migrationProgress") { + const update = msg as MigrationProgressMessage setProgressEntries((prev) => { const existing = prev.findIndex((e) => e.item === update.item) const entry: ProgressEntry = { @@ -278,8 +302,8 @@ const MigrationWizard: Component = (props) => { }) } - if (msg?.type === "legacyMigrationSessionProgress") { - const update = msg as LegacyMigrationSessionProgressMessage + if (msg.type === "migrationSessionProgress") { + const update = msg as MigrationSessionProgressMessage setSessionSummary((prev) => updateSessionSummary(prev, createSessionItem(update.session, update.error), update.phase), ) @@ -292,8 +316,8 @@ const MigrationWizard: Component = (props) => { }) } - if (msg?.type === "legacyMigrationComplete") { - const complete = msg as LegacyMigrationCompleteMessage + if (msg.type === "migrationComplete") { + const complete = msg as MigrationCompleteMessage setResults(complete.results) setRunning(false) const hasErrors = complete.results.some((r) => r.status === "error") @@ -305,7 +329,7 @@ const MigrationWizard: Component = (props) => { } window.addEventListener("message", handler) - vscode.postMessage({ type: "requestLegacyMigrationData" }) + vscode.postMessage({ type: "requestMigrationData", source, operationId }) onCleanup(() => window.removeEventListener("message", handler)) }) @@ -314,105 +338,91 @@ const MigrationWizard: Component = (props) => { // --------------------------------------------------------------------------- const handleSkip = () => { + if (!config.persist) { + props.onBack() + return + } vscode.postMessage({ type: "skipLegacyMigration" }) props.onBack() } - const handleStartMigration = () => { - const selectedProviderNames = migrateProviders() + const emptyAutoApproval = (): MigrationAutoApprovalSelections => ({ + commandRules: false, + readPermission: false, + writePermission: false, + executePermission: false, + mcpPermission: false, + taskPermission: false, + }) + + const autoApprovalSelections = (): MigrationAutoApprovalSelections => { + if (!migrateAutoApproval()) return emptyAutoApproval() + const settings = legacySettings() + return { + commandRules: + settings?.autoApprovalEnabled !== undefined || + Boolean(settings?.allowedCommands?.length) || + Boolean(settings?.deniedCommands?.length), + readPermission: + settings?.alwaysAllowReadOnly !== undefined || settings?.alwaysAllowReadOnlyOutsideWorkspace !== undefined, + writePermission: settings?.alwaysAllowWrite !== undefined, + executePermission: settings?.alwaysAllowExecute !== undefined, + mcpPermission: settings?.alwaysAllowMcp !== undefined, + taskPermission: settings?.alwaysAllowModeSwitch !== undefined || settings?.alwaysAllowSubtasks !== undefined, + } + } + + const selections = (sessions: { id: string; force?: boolean }[] = []) => ({ + providers: migrateProviders() ? providers() - .filter((p) => p.supported && p.hasApiKey) - .map((p) => p.profileName) - : [] - const selectedMcpNames = migrateMcpServers() ? mcpServers().map((s) => s.name) : [] - const selectedModesSlugs = migrateModes() ? customModes().map((m) => m.slug) : [] + .filter((provider) => provider.supported && provider.hasApiKey) + .map((provider) => provider.profileName) + : [], + mcpServers: migrateMcpServers() ? mcpServers().map((server) => server.name) : [], + customModes: migrateModes() ? customModes().map((mode) => mode.slug) : [], + sessions, + defaultModel: migrateDefaultModel(), + settings: { + autoApproval: autoApprovalSelections(), + language: migrateLanguage(), + autocomplete: migrateAutocomplete(), + }, + }) - const autoApproval: MigrationAutoApprovalSelections = migrateAutoApproval() - ? { - commandRules: - legacySettings()?.autoApprovalEnabled !== undefined || - Boolean(legacySettings()?.allowedCommands?.length) || - Boolean(legacySettings()?.deniedCommands?.length), - readPermission: - legacySettings()?.alwaysAllowReadOnly !== undefined || - legacySettings()?.alwaysAllowReadOnlyOutsideWorkspace !== undefined, - writePermission: legacySettings()?.alwaysAllowWrite !== undefined, - executePermission: legacySettings()?.alwaysAllowExecute !== undefined, - mcpPermission: legacySettings()?.alwaysAllowMcp !== undefined, - taskPermission: - legacySettings()?.alwaysAllowModeSwitch !== undefined || - legacySettings()?.alwaysAllowSubtasks !== undefined, - } - : { - commandRules: false, - readPermission: false, - writePermission: false, - executePermission: false, - mcpPermission: false, - taskPermission: false, - } - - // Build progress entries + const handleStartMigration = () => { + const selected = selections(migrateSessions() ? sessions().map((session) => ({ id: session.id })) : []) + const approval = selected.settings.autoApproval const entries: ProgressEntry[] = [ - ...selectedProviderNames.map((name) => ({ item: name, group: "providers", status: "pending" as const })), - ...selectedMcpNames.map((name) => ({ item: name, group: "mcpServers", status: "pending" as const })), - ...selectedModesSlugs.map((slug) => { - const mode = customModes().find((m) => m.slug === slug) - return { item: mode?.name ?? slug, group: "customModes", status: "pending" as const } - }), - ...(migrateSessions() - ? sessions().map((session) => ({ item: session.id, group: "sessions", status: "pending" as const })) - : []), - ...(migrateDefaultModel() && defaultModel() + ...selected.providers.map((item) => ({ item, group: "providers", status: "pending" as const })), + ...selected.mcpServers.map((item) => ({ item, group: "mcpServers", status: "pending" as const })), + ...selected.customModes.map((item) => ({ item, group: "customModes", status: "pending" as const })), + ...selected.sessions.map((session) => ({ item: session.id, group: "sessions", status: "pending" as const })), + ...(selected.defaultModel && defaultModel() ? [{ item: "Default model", group: "defaultModel", status: "pending" as const }] : []), - ...(autoApproval.commandRules - ? [{ item: "Command rules", group: "autoApproval", status: "pending" as const }] - : []), - ...(autoApproval.readPermission - ? [{ item: "Read permission", group: "autoApproval", status: "pending" as const }] - : []), - ...(autoApproval.writePermission - ? [{ item: "Write permission", group: "autoApproval", status: "pending" as const }] - : []), - ...(autoApproval.executePermission - ? [{ item: "Execute permission", group: "autoApproval", status: "pending" as const }] - : []), - ...(autoApproval.mcpPermission - ? [{ item: "MCP permission", group: "autoApproval", status: "pending" as const }] - : []), - ...(autoApproval.taskPermission - ? [{ item: "Task permission", group: "autoApproval", status: "pending" as const }] - : []), - ...(migrateLanguage() && legacySettings()?.language + ...( + [ + [approval.commandRules, "Command rules"], + [approval.readPermission, "Read permission"], + [approval.writePermission, "Write permission"], + [approval.executePermission, "Execute permission"], + [approval.mcpPermission, "MCP permission"], + [approval.taskPermission, "Task permission"], + ] as const + ).flatMap(([enabled, item]) => (enabled ? [{ item, group: "autoApproval", status: "pending" as const }] : [])), + ...(selected.settings.language && legacySettings()?.language ? [{ item: "Language preference", group: "language", status: "pending" as const }] : []), - ...(migrateAutocomplete() && legacySettings()?.autocomplete + ...(selected.settings.autocomplete && legacySettings()?.autocomplete ? [{ item: "Autocomplete settings", group: "autocomplete", status: "pending" as const }] : []), ] - setProgressEntries(entries) setSessionSummary(createSessionSummary()) setSessionProgress(undefined) setRunning(true) setPhase("migrating") - - vscode.postMessage({ - type: "startLegacyMigration", - selections: { - providers: selectedProviderNames, - mcpServers: selectedMcpNames, - customModes: selectedModesSlugs, - sessions: migrateSessions() ? sessions().map((session) => ({ id: session.id })) : [], - defaultModel: migrateDefaultModel(), - settings: { - autoApproval, - language: migrateLanguage(), - autocomplete: migrateAutocomplete(), - }, - }, - }) + vscode.postMessage({ type: "startMigration", source, operationId, selections: selected }) } const handleForceReimport = (ids: string[]) => { @@ -424,13 +434,14 @@ const MigrationWizard: Component = (props) => { setPhase("migrating") setSessionProgress(undefined) setResults((prev) => prev.filter((item) => item.category !== "session")) - setProgressEntries((prev) => { - const keep = prev.filter((item) => item.group !== "sessions") - const next = ids.map((id) => ({ item: id, group: "sessions", status: "pending" as const })) - return [...keep, ...next] - }) + setProgressEntries((prev) => [ + ...prev.filter((item) => item.group !== "sessions"), + ...ids.map((item) => ({ item, group: "sessions", status: "pending" as const })), + ]) vscode.postMessage({ - type: "startLegacyMigration", + type: "startMigration", + source, + operationId, selections: { providers: [], mcpServers: [], @@ -438,14 +449,7 @@ const MigrationWizard: Component = (props) => { sessions: ids.map((id) => ({ id, force: true })), defaultModel: false, settings: { - autoApproval: { - commandRules: false, - readPermission: false, - writePermission: false, - executePermission: false, - mcpPermission: false, - taskPermission: false, - }, + autoApproval: emptyAutoApproval(), language: false, autocomplete: false, }, @@ -458,12 +462,18 @@ const MigrationWizard: Component = (props) => { } const handleDone = () => { + if (!config.persist) { + vscode.postMessage({ type: "loadSessions" }) + props.onComplete() + return + } + if (running()) { dialog.show(() => ( { vscode.postMessage({ type: "finalizeLegacyMigration" }) - if (clearLegacyData()) { + if (config.cleanup && clearLegacyData()) { vscode.postMessage({ type: "clearLegacyData" }) } props.onComplete() @@ -473,7 +483,7 @@ const MigrationWizard: Component = (props) => { return } vscode.postMessage({ type: "finalizeLegacyMigration" }) - if (clearLegacyData()) { + if (config.cleanup && clearLegacyData()) { vscode.postMessage({ type: "clearLegacyData" }) } props.onComplete() @@ -916,7 +926,7 @@ const MigrationWizard: Component = (props) => { diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx index 60fa97e37e0..ea15459c9bc 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AboutKiloCodeTab.tsx @@ -5,14 +5,14 @@ import { showToast } from "@kilocode/kilo-ui/toast" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" import { useConfig } from "../../context/config" -import type { Config, ConnectionState, ExtensionMessage } from "../../types/messages" +import type { Config, ConnectionState, ExtensionMessage, MigrationSource } from "../../types/messages" import { buildExport, parseImport, MAX_IMPORT_SIZE } from "./settings-io" export interface AboutKiloCodeTabProps { port: number | null connectionState: ConnectionState extensionVersion?: string - onMigrateClick?: () => void // legacy-migration + onMigrationClick?: (source: MigrationSource) => void // legacy-migration } const AboutKiloCodeTab: Component = (props) => { @@ -328,21 +328,19 @@ const AboutKiloCodeTab: Component = (props) => { > {language.t("settings.aboutKiloCode.legacyMigration.description")}

- +
+ + +
{/* legacy-migration end */} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 30c240b4f97..5244ccddd7b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -21,7 +21,7 @@ const SHARE_OPTIONS: ShareOption[] = [ ] const ExperimentalTab: Component = () => { - const { config, updateConfig } = useConfig() + const { config, features, updateConfig } = useConfig() const language = useLanguage() const vscode = useVSCode() const [active, setActive] = createSignal(false) @@ -171,7 +171,7 @@ const ExperimentalTab: Component = () => { { }} /> + + + + updateExperimental("sandbox", checked)} + hideLabel + > + {language.t("settings.experimental.sandbox.title")} + + + {/* Tool toggles */} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index 4ea7681a879..ed6aa59799a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -93,7 +93,7 @@ function providerFields(provider: ProviderId | undefined): Array<{ key: string; } const IndexingTab: Component = () => { - const { globalConfig, projectConfig, updateGlobalConfig, updateProjectConfig } = useConfig() + const { globalConfig, projectConfig, settings, updateGlobalConfig, updateProjectConfig, updateSetting } = useConfig() const indexing = useIndexing() const embeds = useKiloEmbeddingModels() const language = useLanguage() @@ -292,12 +292,24 @@ const IndexingTab: Component = () => { : language.t("settings.indexing.enable.description") } tag={() => tag(scope(), [["enabled"]])} - last > {language.t("settings.indexing.enable.title")} + + updateSetting("indexing.showButtonWhenDisabled", checked)} + hideLabel + > + {language.t("settings.indexing.showButton.title")} + + diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx new file mode 100644 index 00000000000..e67ec17c7aa --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx @@ -0,0 +1,43 @@ +import { Component, createMemo } from "solid-js" +import { Card } from "@kilocode/kilo-ui/card" +import { Switch } from "@kilocode/kilo-ui/switch" +import { useConfig } from "../../context/config" +import { useLanguage } from "../../context/language" +import SettingsRow from "./SettingsRow" + +const description = "sandbox-network-description" + +const SandboxingTab: Component = () => { + const { config, updateConfig } = useConfig() + const language = useLanguage() + const experimental = createMemo(() => config().experimental ?? {}) + + return ( + + + + updateConfig({ + experimental: { + ...experimental(), + sandbox_restrict_network: checked, + }, + }) + } + hideLabel + > + {language.t("settings.sandboxing.network.title")} + + + + ) +} + +export default SandboxingTab diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index 59e8206bff3..2ec4c32d945 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, createEffect, on, Show } from "solid-js" +import { Component, createSignal, createEffect, createMemo, on, Show } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" import { Tabs } from "@kilocode/kilo-ui/tabs" import { Button } from "@kilocode/kilo-ui/button" @@ -23,22 +23,26 @@ import ExperimentalTab from "./ExperimentalTab" import LanguageTab from "./LanguageTab" import AboutKiloCodeTab from "./AboutKiloCodeTab" import IndexingTab from "./IndexingTab" +import SandboxingTab from "./SandboxingTab" +import * as Sandboxing from "./sandboxing" import { useServer } from "../../context/server" +import type { MigrationSource } from "../../types/messages" export interface SettingsProps { tab?: string onTabChange?: (tab: string) => void - onMigrateClick?: () => void // legacy-migration + onMigrationClick?: (source: MigrationSource) => void // legacy-migration } const Settings: Component = (props) => { const server = useServer() const language = useLanguage() const vscode = useVSCode() - const { isDirty, saving, saveError, saveConfig, discardConfig, features } = useConfig() + const { config, loading, isDirty, saving, saveError, saveConfig, discardConfig, features } = useConfig() const session = useSession() const [active, setActive] = createSignal(props.tab ?? "models") const [errorExpanded, setErrorExpanded] = createSignal(false) + const sandboxing = createMemo(() => Sandboxing.visible(features(), config())) const busyCount = () => Object.values(session.allStatusMap()).filter((s) => s.type === "busy").length @@ -106,6 +110,11 @@ const Settings: Component = (props) => { onTabChange("providers") }) + createEffect(() => { + if (loading() || sandboxing() || active() !== "sandboxing") return + onTabChange("experimental") + }) + const onTabChange = (tab: string) => { setActive(tab) props.onTabChange?.(tab) @@ -200,6 +209,12 @@ const Settings: Component = (props) => { {language.t("settings.experimental.title")} + + + + {language.t("settings.sandboxing.title")} + + {language.t("settings.language.title")} @@ -265,6 +280,12 @@ const Settings: Component = (props) => {

{language.t("settings.experimental.title")}

+ + +

{language.t("settings.sandboxing.title")}

+ +
+

{language.t("settings.language.title")}

@@ -275,7 +296,7 @@ const Settings: Component = (props) => { port={server.serverInfo()?.port ?? null} connectionState={server.connectionState()} extensionVersion={server.extensionVersion()} - onMigrateClick={props.onMigrateClick} + onMigrationClick={props.onMigrationClick} />
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx index 5596527526b..f913d9426c9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SettingsRow.tsx @@ -4,6 +4,7 @@ import { Component, JSX, Show } from "solid-js" const SettingsRow: Component<{ title: string description?: string + descriptionId?: string tag?: () => string | undefined last?: boolean children: JSX.Element @@ -32,7 +33,9 @@ const SettingsRow: Component<{ {(tag) => {tag()}}
{props.description !== null && props.description !== undefined && ( -
{props.description}
+
+ {props.description} +
)}
{props.children}
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts b/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts new file mode 100644 index 00000000000..023549e77d7 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/settings/sandboxing.ts @@ -0,0 +1,5 @@ +import type { Config, FeatureFlags } from "../../types/messages" + +export function visible(features: FeatureFlags, config: Config) { + return features.sandboxControls && config.experimental?.sandbox === true +} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index ae3589f86d8..485d3b80032 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -5,7 +5,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" -import { freeDataLabel, isDataCollectedModel, sanitizeName } from "./model-selector-utils" +import { freeDataLabel, hasByok, isDataCollectedModel, sanitizeName } from "./model-selector-utils" import { avgPrice, fmtAttemptCost, fmtCachedPrice, fmtPrice, fmtTerminalBenchScore } from "./model-preview-utils" interface Props { @@ -91,11 +91,14 @@ export const ModelPreview: Component = (props) => { })()}
- + - + {freeLabel()} + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index df7ff77f087..e856d5c1f6e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -38,6 +38,7 @@ import { providerSortKey, isFree, isDataCollectedModel, + hasByok, freeDataLabel, buildTriggerLabel, sanitizeName, @@ -936,13 +937,18 @@ export const ModelSelectorBase: Component = (props) => { ) })()} - + - + {freeLabel()} + + + BYOK + + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index f5a183f1845..5c75b79782a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -26,6 +26,10 @@ export function isDataCollectedModel(model: Pick): boolean { + return model.hasUserByokAvailable === true +} + export function freeDataLabel(_free: string, data: string): string { return data } diff --git a/packages/kilo-vscode/webview-ui/src/context/config.tsx b/packages/kilo-vscode/webview-ui/src/context/config.tsx index 301721c5cb4..d996c6eb9a0 100644 --- a/packages/kilo-vscode/webview-ui/src/context/config.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/config.tsx @@ -58,7 +58,7 @@ export const ConfigProvider: ParentComponent = (props) => { const [globalConfig, setGlobalConfig] = createSignal({}) const [projectConfig, setProjectConfig] = createSignal({}) const [settings, setSettings] = createSignal>({}) - const [features, setFeatures] = createSignal({ indexing: false }) + const [features, setFeatures] = createSignal({ indexing: false, sandboxControls: false }) const [loading, setLoading] = createSignal(true) const [draft, setDraft] = createSignal>({}) const [globalDraft, setGlobalDraft] = createSignal>({}) @@ -96,6 +96,12 @@ export const ConfigProvider: ParentComponent = (props) => { }) return } + if (message.type === "indexingSettingsLoaded") { + mergeSettings({ + "indexing.showButtonWhenDisabled": message.settings.showButtonWhenDisabled, + }) + return + } if (message.type === "configLoaded") { // Skip if a save is in-flight — a stale configLoaded must not overwrite // the optimistically-updated state while the write is being confirmed. @@ -177,6 +183,7 @@ export const ConfigProvider: ParentComponent = (props) => { const requestInitialData = () => { vscode.postMessage({ type: "requestConfig" }) vscode.postMessage({ type: "requestAutocompleteSettings" }) + vscode.postMessage({ type: "requestIndexingSettings" }) } // Request config immediately; if the extension's httpClient is not yet ready, diff --git a/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts b/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts index 5f045684e41..86c33bd7a29 100644 --- a/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/indexing-utils.ts @@ -1,4 +1,11 @@ -import type { ExtensionMessage, IndexingStatus } from "../types/messages" +import type { Config, ExtensionMessage, IndexingStatus } from "../types/messages" + +export function indexingButtonVisible(feature: boolean, show: boolean, config: Config, global: Config) { + if (!feature) return false + if (show) return true + if (global.indexing?.enabled === true) return true + return config.indexing?.enabled === true +} export function formatIndexingLabel(status: IndexingStatus): string { if (status.state === "In Progress") { diff --git a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts index 066df583295..59b78335511 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts @@ -1,4 +1,6 @@ -import type { Message, SessionStatusInfo } from "../types/messages" +import type { Message, Part, SessionInfo, SessionStatusInfo } from "../types/messages" + +export type RevertBoundary = Pick, "messageID" | "partID"> export interface MessageTurn { id: string @@ -55,24 +57,37 @@ function target(messages: Message[], index: number, id: string, parts?: (msg: Me return id } +function visibleMessage(id: string, revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return true + return id === revert.messageID && !!revert.partID +} + +export function visibleParts(id: string, parts: Part[], revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return parts + if (id !== revert.messageID || !revert.partID) return [] + const idx = parts.findIndex((part) => part.id === revert.partID) + return idx < 0 ? [] : parts.slice(0, idx) +} + export function messageTurns( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): MessageTurn[] { const result: MessageTurn[] = [] const lead: Message[] = [] const by = new Map() + const projected = (msg: Message) => visibleParts(msg.id, parts?.(msg) ?? msg.parts ?? [], revert) let compact: { turn: MessageTurn; index: number } | undefined for (const msg of messages) { + if (!visibleMessage(msg.id, revert)) continue if (msg.role === "user") { - if (boundary && msg.id >= boundary) break const turn = { id: msg.id, user: msg, assistant: [] } const item = { turn, index: result.length } result.push(turn) by.set(msg.id, item) - if (isCompact(msg, parts)) compact = item + if (isCompact(msg, projected)) compact = item continue } @@ -105,10 +120,10 @@ export function messageTurns( export function visibleMessages( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): Message[] { - return messageTurns(messages, boundary, parts).flatMap((turn) => + return messageTurns(messages, revert, parts).flatMap((turn) => turn.partial ? turn.assistant : [turn.user, ...turn.assistant], ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 9e7798119a7..ae448a3261b 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -1,3 +1,4 @@ +import { reconcile } from "solid-js/store" import type { Message, Part, ToolPart } from "../types/messages" export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..." @@ -85,6 +86,10 @@ export function buildSessionToolParts( return tools } +export function reconcileSessionToolParts(tools: ToolPart[]) { + return reconcile(tools, { key: "id" }) +} + export function upsertSessionToolPart( current: ToolPart[], part: Part, diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 01788f5d667..235b3ea1d37 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -57,6 +57,7 @@ import { buildCostBreakdown, buildSessionToolParts, childID, + reconcileSessionToolParts, removeSessionToolPart, removeSessionToolPartsForMessage, upsertSessionToolPart, @@ -1290,12 +1291,16 @@ export const SessionProvider: ParentComponent = (props) => { return true } + function setTools(sessionID: string, tools: ToolPart[]) { + setStore("toolParts", sessionID, reconcileSessionToolParts(tools)) + } + function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record) { const tools = buildSessionToolParts( messages, (msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts, ) - setStore("toolParts", sessionID, tools) + setTools(sessionID, tools) } function messageParts(messages: Message[]): Record { @@ -1310,16 +1315,17 @@ export const SessionProvider: ParentComponent = (props) => { const sid = sessionID ?? part.sessionID if (!sid) return if (part.type !== "tool") return - setStore("toolParts", sid, (tools = []) => upsertSessionToolPart(tools, part, { id: messageID, sessionID: sid })) + const tools = upsertSessionToolPart(store.toolParts[sid] ?? [], part, { id: messageID, sessionID: sid }) + setTools(sid, tools) } function dropToolPart(sessionID: string | undefined, partID: string) { if (!sessionID) return - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPart(tools, partID)) + setTools(sessionID, removeSessionToolPart(store.toolParts[sessionID] ?? [], partID)) } function dropMessageTools(sessionID: string, messageID: string) { - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPartsForMessage(tools, messageID)) + setTools(sessionID, removeSessionToolPartsForMessage(store.toolParts[sessionID] ?? [], messageID)) } function handleMessagesLoaded( @@ -2550,8 +2556,10 @@ export const SessionProvider: ParentComponent = (props) => { const userMessages = createMemo(() => messages().filter((m) => m.role === "user")) function visible(sessionID: string) { - return filterVisibleMessages(store.messages[sessionID] ?? [], store.sessions[sessionID]?.revert?.messageID, (msg) => - getParts(msg.id), + return filterVisibleMessages( + store.messages[sessionID] ?? [], + store.sessions[sessionID]?.revert ?? undefined, + (msg) => getParts(msg.id), ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts index c6445d4c3c4..89ac134335a 100644 --- a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts +++ b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts @@ -1,5 +1,5 @@ import type { Message, Part } from "../types/messages" -import type { MessageTurn } from "./session-queue" +import { visibleParts, type MessageTurn, type RevertBoundary } from "./session-queue" interface TranscriptMeta { turn: string @@ -46,6 +46,7 @@ export interface TranscriptOptions { queued?: ReadonlySet live?: ReadonlySet hidden?: (id: string) => boolean + revert?: RevertBoundary } export interface TranscriptPartition { @@ -127,6 +128,8 @@ export function transcriptRows( ): TranscriptRow[] { const size = Math.max(1, Math.floor(opts.size ?? 8)) const rows: TranscriptRow[] = [] + const parts = (id: string) => visibleParts(id, getParts(id), opts.revert) + const terminal = (msg: Message) => !(opts.revert?.partID && msg.id === opts.revert.messageID) for (const turn of turns) { const meta = { @@ -135,7 +138,7 @@ export function transcriptRows( queued: opts.queued?.has(turn.id) === true, live: opts.live?.has(turn.id) === true, } - const copied = copy(turn.assistant, getParts) + const copied = copy(turn.assistant, parts) if (!turn.partial) { rows.push({ @@ -143,27 +146,27 @@ export function transcriptRows( type: "user", key: `${turn.id}:user`, message: turn.user, - parts: getParts(turn.user.id), - interrupted: turn.assistant.some((msg) => msg.error?.name === "MessageAbortedError"), + parts: parts(turn.user.id), + interrupted: turn.assistant.some((msg) => terminal(msg) && msg.error?.name === "MessageAbortedError"), answered: turn.assistant.length > 0, }) } for (const msg of turn.assistant) { - const parts = getParts(msg.id) - if (parts.length === 0) { + const visible = parts(msg.id) + if (visible.length === 0) { rows.push({ ...meta, type: "assistant", key: `${turn.id}:assistant:${msg.id}:empty`, message: msg, - parts, + parts: visible, copy: copied, }) continue } - for (let start = 0; start < parts.length; start += size) { - const chunk = parts.slice(start, start + size) + for (let start = 0; start < visible.length; start += size) { + const chunk = visible.slice(start, start + size) rows.push({ ...meta, type: "assistant", @@ -181,7 +184,7 @@ export function transcriptRows( } const failed = turn.assistant.find( - (msg) => msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, + (msg) => terminal(msg) && msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, ) if (failed?.error) { rows.push({ ...meta, type: "error", key: `${turn.id}:error:${failed.id}`, message: failed, error: failed.error }) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index cd51e99f228..b6c5a186551 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -283,6 +283,11 @@ export const dict = { "prompt.action.resetModel": "إعادة تعيين النموذج إلى الافتراضي", "prompt.action.enhanceDescription": "زر «حسّن الموجه» يطوّر موجهك بإضافة سياق أو توضيح أو إعادة صياغة. جرّب اكتب موجه هنا ثم اضغط الزر مرة ثانية وشوف النتيجة.", + "prompt.action.sandbox.enable": "تفعيل sandbox", + "prompt.action.sandbox.disable": "تعطيل sandbox", + "prompt.action.sandbox.enabled": "sandbox مُفعّل. أوامر shell الخاصة بالوكيل مقتصرة على مجلدات المشروع و Kilo.", + "prompt.action.sandbox.disabled": + "sandbox مُعطّل. انقر لتقييد كتابة أوامر shell الخاصة بالوكيل على مجلدات المشروع و Kilo.", "speechToText.tooltip.start": "بدء الإدخال الصوتي باستخدام Kilo Gateway", "speechToText.tooltip.stop": "إيقاف التقاط الصوت", @@ -811,7 +816,7 @@ export const dict = { "settings.providers.tag.customProvider": "مزود مخصص", "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك", "settings.providers.action.signInChatGPT": "تسجيل الدخول باستخدام ChatGPT", - "settings.providers.custom.description": "أضف مزوداً متوافقاً مع OpenAI عبر عنوان URL الأساسي.", + "settings.providers.custom.description": "أضف مزودًا مخصصًا عبر عنوان URL الأساسي.", "settings.providers.subagentModel.title": "نموذج الوكيل الفرعي", "settings.providers.subagentModel.description": "النموذج الافتراضي وجهد التفكير للوكلاء الفرعيين لـ task-tool. اتركه فارغًا ليرث نموذج الوكيل المستدعي.", @@ -819,11 +824,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "إخفاء نماذج Kilo Gateway التي قد يستخدم مزودوها مطالباتك لأغراض التدريب.", "settings.providers.modeModels": "نموذج لكل وضع", - "settings.providers.custom.note": "أضف موفرًا متوافقًا مع OpenAI عبر عنوان URL الأساسي.", + "settings.providers.custom.note": "أضف مزودًا مخصصًا عبر عنوان URL الأساسي.", "settings.providers.modeModels.description": "تجاوز النموذج الافتراضي لأوضاع محددة. إذا لم يتم التعيين، يتم استخدام النموذج الافتراضي العام.", "provider.custom.title": "مزود مخصص", - "provider.custom.description.prefix": "قم بتكوين مزود متوافق مع OpenAI. انظر ", + "provider.custom.description.prefix": "قم بتكوين مزود مخصص. انظر ", "provider.custom.description.link": "وثائق تكوين المزود", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "معرف المزود", @@ -1110,6 +1115,9 @@ export const dict = { "settings.indexing.title": "الفهرسة", "settings.indexing.enable.title": "تمكين الفهرسة", "settings.indexing.enable.description": "تشغيل أو إيقاف فهرسة قاعدة الكود الدلالية.", + "settings.indexing.showButton.title": "إظهار الزر عند تعطيل الفهرسة", + "settings.indexing.showButton.description": + "إظهار زر الفهرسة أسفل حقل الإدخال عندما تكون الفهرسة معطلة. إذا كان الزر مخفيًا، فافتح الإعدادات > الفهرسة لتفعيل الفهرسة.", "settings.indexing.globalEnable.title": "تمكين عام", "settings.indexing.globalEnable.description": "تمكين الفهرسة لكل مساحة عمل.", "settings.indexing.projectEnable.title": "تمكين لهذا المشروع", @@ -1295,6 +1303,10 @@ export const dict = { "settings.models.speechToTextModel.description": "اختر نموذج نسخ Kilo Gateway للإدخال الصوتي.", "settings.experimental.continueOnDeny.title": "المتابعة عند الرفض", "settings.experimental.continueOnDeny.description": "متابعة حلقة الوكيل عند رفض الإذن", + "settings.sandboxing.title": "العزل", + "settings.sandboxing.network.title": "تقييد الوصول إلى الشبكة", + "settings.sandboxing.network.description": + "احظر الوصول الصادر إلى الشبكة من الأوامر الصادرة عن النموذج وأدوات HTTP. تعمل خوادم MCP المحلية وخطافات المكونات الإضافية خارج هذا التقييد. تظل حركة مرور استدلال الموفّر والنموذج متاحة.", "settings.experimental.mcpTimeout.title": "مهلة MCP (مللي ثانية)", "settings.experimental.mcpTimeout.description": "مهلة طلبات خادم MCP بالمللي ثانية", "settings.experimental.remote.title": "التحكم Remote", @@ -1425,6 +1437,9 @@ export const dict = { "settings.agentBehaviour.workflows.empty": "لم يتم تهيئة أوامر مخصصة. أضف أوامر إلى opencode.json لرؤيتها هنا.", "settings.agentBehaviour.workflows.detail.description": "الوصف", "settings.agentBehaviour.workflows.detail.template": "القالب", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "تشغيل أوامر shell الخاصة بالوكيل داخل sandbox على مستوى نظام التشغيل يقيّد الكتابة على مجلدات حالة المشروع و Kilo", "settings.autoApprove.description": "تحديد كيفية السماح بتشغيل الأدوات. معظم الأدوات معينة افتراضياً على السماح. doom_loop و external_directory معينة افتراضياً على السؤال.", @@ -1520,6 +1535,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "ترحيل الإصدار القديم", "settings.aboutKiloCode.legacyMigration.description": "ترحيل الإعدادات من تثبيت سابق لـ Kilo Code، بما في ذلك مفاتيح API لمقدمي الخدمة والنموذج الافتراضي.", + "settings.aboutKiloCode.rooImport.description": "استيراد سجل المحادثات من تثبيت Roo Code.", + "settings.aboutKiloCode.rooImport.button": "استيراد الجلسات من Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "ما الجديد في Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 0a2b9d29ff9..78a2294ffd8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -287,6 +287,12 @@ export const dict = { "prompt.action.enhanceDescription": "O botão 'Aprimorar prompt' ajuda a melhorar seu pedido fornecendo contexto adicional, esclarecimentos ou reformulações. Tente digitar um pedido aqui e clique no botão novamente para ver como funciona.", "prompt.action.indexing": "Configurações de indexação", + "prompt.action.sandbox.enable": "Ativar sandbox", + "prompt.action.sandbox.disable": "Desativar sandbox", + "prompt.action.sandbox.enabled": + "Sandbox ativado. Os comandos shell do agente estão limitados aos diretórios do projeto e do Kilo.", + "prompt.action.sandbox.disabled": + "Sandbox desativado. Clique para limitar as escritas dos comandos shell do agente aos diretórios do projeto e do Kilo.", "speechToText.tooltip.start": "Iniciar entrada de voz com o Kilo Gateway", "speechToText.tooltip.stop": "Parar captura", @@ -823,7 +829,7 @@ export const dict = { "settings.providers.tag.customProvider": "Provedor personalizado", "settings.providers.connected.environmentDescription": "Conectado a partir das suas variáveis de ambiente", "settings.providers.action.signInChatGPT": "Entrar com ChatGPT", - "settings.providers.custom.description": "Adicione um provedor compatível com OpenAI pela URL base.", + "settings.providers.custom.description": "Adicione um provedor personalizado pela URL base.", "settings.providers.subagentModel.title": "Modelo de Subagente", "settings.providers.subagentModel.description": "Modelo padrão e esforço de raciocínio para subagentes do task-tool. Deixe em branco para herdar o modelo do agente chamador.", @@ -831,11 +837,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Oculte os modelos do Kilo Gateway cujos provedores podem usar seus prompts para treinamento.", "settings.providers.modeModels": "Modelo por Modo", - "settings.providers.custom.note": "Adicione um provedor compatível com OpenAI por URL base.", + "settings.providers.custom.note": "Adicione um provedor personalizado por URL base.", "settings.providers.modeModels.description": "Substitua o modelo padrão para modos específicos. Se não definido, o modelo padrão global é usado.", "provider.custom.title": "Provedor personalizado", - "provider.custom.description.prefix": "Configure um provedor compatível com OpenAI. Veja a ", + "provider.custom.description.prefix": "Configure um provedor personalizado. Veja a ", "provider.custom.description.link": "documentação de configuração de provedores", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID do provedor", @@ -1130,6 +1136,9 @@ export const dict = { "settings.indexing.title": "Indexação", "settings.indexing.enable.title": "Ativar indexação", "settings.indexing.enable.description": "Ativar ou desativar a indexação semântica da base de código.", + "settings.indexing.showButton.title": "Exibir o botão quando a indexação estiver desativada", + "settings.indexing.showButton.description": + "Exiba o botão de indexação abaixo do campo de entrada enquanto a indexação estiver desativada. Se o botão estiver oculto, abra Configurações > Indexação para ativar a indexação.", "settings.indexing.globalEnable.title": "Habilitar globalmente", "settings.indexing.globalEnable.description": "Habilitar indexação para todos os workspaces.", "settings.indexing.projectEnable.title": "Habilitar para este projeto", @@ -1327,6 +1336,10 @@ export const dict = { "Escolha o modelo de transcrição do Kilo Gateway para entrada de voz.", "settings.experimental.continueOnDeny.title": "Continuar ao negar", "settings.experimental.continueOnDeny.description": "Continuar o loop do agente quando uma permissão é negada", + "settings.sandboxing.title": "Isolamento em sandbox", + "settings.sandboxing.network.title": "Restringir acesso à rede", + "settings.sandboxing.network.description": + "Bloqueie o acesso de saída à rede para comandos originados pelo modelo e ferramentas HTTP. Servidores MCP locais e hooks de plugins são executados fora dessa restrição. O tráfego de inferência de provedores e modelos permanece disponível.", "settings.experimental.mcpTimeout.title": "Tempo limite MCP (ms)", "settings.experimental.mcpTimeout.description": "Tempo limite para solicitações do servidor MCP em milissegundos", "settings.experimental.remote.title": "Controle Remote", @@ -1463,6 +1476,9 @@ export const dict = { "Nenhum comando personalizado configurado. Adicione comandos ao opencode.json para vê-los aqui.", "settings.agentBehaviour.workflows.detail.description": "Descrição", "settings.agentBehaviour.workflows.detail.template": "Modelo", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Executar os comandos shell do agente dentro de um sandbox a nível de sistema operacional que restringe escritas aos diretórios de estado do projeto e do Kilo", "settings.autoApprove.description": "Defina como as ferramentas têm permissão para serem executadas. A maioria das ferramentas tem o padrão Permitir. doom_loop e external_directory têm o padrão Perguntar.", @@ -1565,6 +1581,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migração Legada", "settings.aboutKiloCode.legacyMigration.description": "Migre as configurações de uma instalação anterior do Kilo Code, incluindo chaves de API de provedor e modelo padrão.", + "settings.aboutKiloCode.rooImport.description": "Importe o histórico de conversas de uma instalação do Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importar sessões do Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "O Que Há de Novo no Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index a543b9971f7..4fb516fcacf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -287,6 +287,12 @@ export const dict = { "prompt.action.resetModel": "Resetuj model na zadani", "prompt.action.enhanceDescription": "Dugme 'Poboljšaj prompt' pomaže poboljšati vaš zahtjev pružajući dodatni kontekst, pojašnjenje ili preformulaciju. Pokušajte upisati zahtjev ovdje i ponovo kliknite na dugme da vidite kako funkcioniše.", + "prompt.action.sandbox.enable": "Omogući sandbox", + "prompt.action.sandbox.disable": "Onemogući sandbox", + "prompt.action.sandbox.enabled": + "Sandbox je omogućen. Shell komande agenta su ograničene na direktorije projekta i Kilo.", + "prompt.action.sandbox.disabled": + "Sandbox je onemogućen. Kliknite da ograničite pisanje shell komandi agenta na direktorije projekta i Kilo.", "speechToText.tooltip.start": "Započni glasovni unos sa Kilo Gateway", "speechToText.tooltip.stop": "Zaustavi hvatanje zvuka", @@ -796,6 +802,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "Auto", "settings.indexing.dimension.title": "Dimenzija vektora", "settings.indexing.enable.description": "Uključite ili isključite semantičko indeksiranje baze koda.", + "settings.indexing.showButton.title": "Prikaži dugme kada je indeksiranje isključeno", + "settings.indexing.showButton.description": + "Prikaži dugme za indeksiranje ispod polja za unos dok je indeksiranje isključeno. Ako je dugme skriveno, otvorite Postavke > Indeksiranje da biste omogućili indeksiranje.", "settings.indexing.enable.title": "Omogući indeksiranje", "settings.indexing.globalEnable.title": "Omogući globalno", "settings.indexing.globalEnable.description": "Omogući indeksiranje za svaki radni prostor.", @@ -864,7 +873,7 @@ export const dict = { "settings.providers.tag.customProvider": "Prilagođeni provajder", "settings.providers.connected.environmentDescription": "Povezano iz vaših varijabli okruženja", "settings.providers.action.signInChatGPT": "Prijavi se putem ChatGPT", - "settings.providers.custom.description": "Dodaj OpenAI-kompatibilan provajder putem osnovnog URL-a.", + "settings.providers.custom.description": "Dodaj prilagođeni provajder putem osnovnog URL-a.", "settings.providers.subagentModel.title": "Model podagenta", "settings.providers.subagentModel.description": "Zadani model i napor zaključivanja za podagente task-tool-a. Ostavite nepodešeno da naslijedi model pozivnog agenta.", @@ -872,11 +881,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Sakrij Kilo Gateway modele čiji pružaoci mogu koristiti vaše promptove za treniranje.", "settings.providers.modeModels": "Model po režimu", - "settings.providers.custom.note": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.", + "settings.providers.custom.note": "Dodajte prilagođenog provajdera putem osnovnog URL-a.", "settings.providers.modeModels.description": "Zamijenite podrazumijevani model za određene režime. Ako nije postavljeno, koristi se globalni podrazumijevani model.", "provider.custom.title": "Prilagođeni provajder", - "provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilan provajder. Pogledaj ", + "provider.custom.description.prefix": "Konfiguriši prilagođeni provajder. Pogledaj ", "provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID provajdera", @@ -1325,6 +1334,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Odaberite Kilo Gateway model za transkripciju za glasovni unos.", "settings.experimental.continueOnDeny.title": "Nastavi pri odbijanju", "settings.experimental.continueOnDeny.description": "Nastavi petlju agenta kada je dozvola odbijena", + "settings.sandboxing.title": "Rad u izoliranom okruženju", + "settings.sandboxing.network.title": "Ograniči pristup mreži", + "settings.sandboxing.network.description": + "Blokiraj odlazni mrežni pristup za naredbe koje potiču od modela i HTTP alate. Lokalni MCP serveri i hookovi dodataka izvršavaju se izvan ovog ograničenja. Saobraćaj za inferenciju pružatelja i modela ostaje dostupan.", "settings.experimental.mcpTimeout.title": "MCP istek vremena (ms)", "settings.experimental.mcpTimeout.description": "Istek vremena za MCP server zahtjeve u milisekundama", "settings.experimental.remote.title": "Remote kontrola", @@ -1457,6 +1470,9 @@ export const dict = { "Nema konfiguriranih prilagođenih komandi. Dodajte komande u opencode.json da ih vidite ovdje.", "settings.agentBehaviour.workflows.detail.description": "Opis", "settings.agentBehaviour.workflows.detail.template": "Predložak", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Pokrenite shell komande agenta unutar sandboxa na nivou operativnog sistema koji ograničava pisanje na direktorije stanja projekta i Kilo", "settings.autoApprove.description": "Definišite kako je dozvoljeno pokretanje alata. Većina alata je podrazumijevano na Dozvoli. doom_loop i external_directory su podrazumijevano na Pitaj.", @@ -1559,6 +1575,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migracija sa prethodne verzije", "settings.aboutKiloCode.legacyMigration.description": "Migrirajte postavke iz prethodne instalacije Kilo Code, uključujući API ključeve provajdera i podrazumijevani model.", + "settings.aboutKiloCode.rooImport.description": "Uvezite historiju razgovora iz instalacije Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Uvezi sesije iz Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Šta je novo u Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index de4d4bb84ec..92ee09fdd3c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -286,6 +286,12 @@ export const dict = { "prompt.action.resetModel": "Nulstil model til standard", "prompt.action.enhanceDescription": "Knappen 'Forbedr prompt' hjælper med at forbedre din forespørgsel ved at give ekstra kontekst, præcisering eller omformulering. Prøv at skrive en forespørgsel her og klik på knappen igen for at se hvordan det virker.", + "prompt.action.sandbox.enable": "Aktivér sandbox", + "prompt.action.sandbox.disable": "Deaktivér sandbox", + "prompt.action.sandbox.enabled": + "Sandbox er aktiveret. Shell-kommandoer for agenten er begrænset til projekt- og Kilo-mapperne.", + "prompt.action.sandbox.disabled": + "Sandbox er deaktiveret. Klik for at begrænse skrivning af shell-kommandoer for agenten til projekt- og Kilo-mapperne.", "speechToText.tooltip.start": "Start stemmeinput med Kilo Gateway", "speechToText.tooltip.stop": "Stop lydoptagelse", @@ -806,6 +812,9 @@ export const dict = { "settings.indexing.title": "Indeksering", "settings.indexing.enable.title": "Aktivér indeksering", "settings.indexing.enable.description": "Slå semantisk kodebase-indeksering til eller fra.", + "settings.indexing.showButton.title": "Vis knappen, når indeksering er slået fra", + "settings.indexing.showButton.description": + "Vis indekseringsknappen under promptfeltet, mens indeksering er slået fra. Hvis knappen er skjult, skal du åbne Indstillinger > Indeksering for at slå indeksering til.", "settings.indexing.globalEnable.title": "Aktivér globalt", "settings.indexing.globalEnable.description": "Aktivér indeksering for alle arbejdsområder.", "settings.indexing.projectEnable.title": "Aktivér for dette projekt", @@ -858,7 +867,7 @@ export const dict = { "settings.providers.tag.customProvider": "Brugerdefineret udbyder", "settings.providers.connected.environmentDescription": "Forbundet fra dine miljøvariabler", "settings.providers.action.signInChatGPT": "Log ind med ChatGPT", - "settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.", + "settings.providers.custom.description": "Tilføj en brugerdefineret udbyder via basis-URL.", "settings.providers.subagentModel.title": "Underagentmodel", "settings.providers.subagentModel.description": "Standardmodel og ræsonnementsindsats for task-tool-underagenter. Lad den være tom for at nedarve den kaldende agents model.", @@ -866,11 +875,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Skjul Kilo Gateway-modeller, hvis udbydere muligvis bruger dine prompts til træning.", "settings.providers.modeModels": "Model pr. tilstand", - "settings.providers.custom.note": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.", + "settings.providers.custom.note": "Tilføj en brugerdefineret udbyder via basis-URL.", "settings.providers.modeModels.description": "Tilsidesæt standardmodellen for bestemte tilstande. Hvis ikke angivet, bruges den globale standardmodel.", "provider.custom.title": "Brugerdefineret udbyder", - "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel udbyder. Se ", + "provider.custom.description.prefix": "Konfigurer en brugerdefineret udbyder. Se ", "provider.custom.description.link": "dokumentation for udbyderkonfiguration", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Udbyder-ID", @@ -1319,6 +1328,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Vælg Kilo Gateway-transskriptionsmodellen til stemmeinput.", "settings.experimental.continueOnDeny.title": "Fortsæt ved afvisning", "settings.experimental.continueOnDeny.description": "Fortsæt agentløkken, når en tilladelse afvises", + "settings.sandboxing.title": "Sandboxing", + "settings.sandboxing.network.title": "Begræns netværksadgang", + "settings.sandboxing.network.description": + "Bloker udgående netværksadgang fra kommandoer, der stammer fra modellen, og HTTP-værktøjer. Lokale MCP-servere og plugin-hooks er ikke underlagt denne begrænsning. Inferenstrafik til udbydere og modeller er fortsat tilgængelig.", "settings.experimental.mcpTimeout.title": "MCP-timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout for MCP-serveranmodninger i millisekunder", "settings.experimental.remote.title": "Remote-styring", @@ -1450,6 +1463,9 @@ export const dict = { "Ingen brugerdefinerede kommandoer konfigureret. Tilføj kommandoer til opencode.json for at se dem her.", "settings.agentBehaviour.workflows.detail.description": "Beskrivelse", "settings.agentBehaviour.workflows.detail.template": "Skabelon", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Kør shell-kommandoer for agenten i en sandbox på operativsystemniveau, der begrænser skrivning til projekt- og Kilo-tilstandsmapperne", "settings.autoApprove.description": "Definer, hvordan værktøjer må køre. De fleste værktøjer er som standard indstillet til Tillad. doom_loop og external_directory er som standard indstillet til Spørg.", @@ -1549,6 +1565,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Legacy-migrering", "settings.aboutKiloCode.legacyMigration.description": "Migrer indstillinger fra en tidligere installation af Kilo Code, herunder udbyder API-nøgler og standardmodel.", + "settings.aboutKiloCode.rooImport.description": "Importer samtalehistorik fra en installation af Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importer sessioner fra Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Nyheder i Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index f7e00f68fdc..03441e4f5b4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -290,6 +290,12 @@ export const dict = { "prompt.action.resetModel": "Modell auf Standard zurücksetzen", "prompt.action.enhanceDescription": "Die Schaltfläche 'Prompt verbessern' hilft, deine Anfrage durch zusätzlichen Kontext, Klarstellungen oder Umformulierungen zu verbessern. Versuche, hier eine Anfrage einzugeben und klicke erneut auf die Schaltfläche, um zu sehen, wie es funktioniert.", + "prompt.action.sandbox.enable": "Sandbox aktivieren", + "prompt.action.sandbox.disable": "Sandbox deaktivieren", + "prompt.action.sandbox.enabled": + "Sandbox ist aktiviert. Shell-Befehle des Agenten sind auf die Projekt- und Kilo-Verzeichnisse beschränkt.", + "prompt.action.sandbox.disabled": + "Sandbox ist deaktiviert. Klicken, um Schreibvorgänge der Shell-Befehle des Agenten auf die Projekt- und Kilo-Verzeichnisse zu beschränken.", "speechToText.tooltip.start": "Spracheingabe mit Kilo Gateway starten", "speechToText.tooltip.stop": "Audioerfassung beenden", @@ -802,6 +808,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "Auto", "settings.indexing.dimension.title": "Vektordimension", "settings.indexing.enable.description": "Semantische Codebasis-Indizierung ein- oder ausschalten.", + "settings.indexing.showButton.title": "Schaltfläche anzeigen, wenn die Indizierung deaktiviert ist", + "settings.indexing.showButton.description": + "Zeigt die Schaltfläche für die Indizierung unter dem Eingabefeld an, solange die Indizierung deaktiviert ist. Wenn die Schaltfläche ausgeblendet ist, öffnen Sie Einstellungen > Indizierung, um die Indizierung zu aktivieren.", "settings.indexing.enable.title": "Indizierung aktivieren", "settings.indexing.globalEnable.title": "Global aktivieren", "settings.indexing.globalEnable.description": "Indizierung für jeden Workspace aktivieren.", @@ -870,7 +879,7 @@ export const dict = { "settings.providers.tag.customProvider": "Benutzerdefinierter Anbieter", "settings.providers.connected.environmentDescription": "Verbunden über Ihre Umgebungsvariablen", "settings.providers.action.signInChatGPT": "Mit ChatGPT anmelden", - "settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter über die Basis-URL hinzu.", + "settings.providers.custom.description": "Fügen Sie einen benutzerdefinierten Anbieter über die Basis-URL hinzu.", "settings.providers.subagentModel.title": "Subagenten-Modell", "settings.providers.subagentModel.description": "Standardmodell und Aufwand für Schlussfolgerungen für task-tool-Subagenten. Leer lassen, um das Modell des aufrufenden Agenten zu übernehmen.", @@ -878,11 +887,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Blendet Kilo-Gateway-Modelle aus, deren Anbieter Ihre Prompts möglicherweise zum Training verwenden.", "settings.providers.modeModels": "Modell pro Modus", - "settings.providers.custom.note": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.", + "settings.providers.custom.note": "Fügen Sie einen benutzerdefinierten Anbieter per Basis-URL hinzu.", "settings.providers.modeModels.description": "Überschreiben Sie das Standardmodell für bestimmte Modi. Wenn nicht festgelegt, wird das globale Standardmodell verwendet.", "provider.custom.title": "Benutzerdefinierter Anbieter", - "provider.custom.description.prefix": "Konfigurieren Sie einen OpenAI-kompatiblen Anbieter. Siehe die ", + "provider.custom.description.prefix": "Konfigurieren Sie einen benutzerdefinierten Anbieter. Siehe die ", "provider.custom.description.link": "Anbieterkonfigurationsdokumentation", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Anbieter-ID", @@ -1344,6 +1353,10 @@ export const dict = { "settings.experimental.continueOnDeny.title": "Bei Ablehnung fortfahren", "settings.experimental.continueOnDeny.description": "Agent-Schleife fortsetzen, wenn eine Berechtigung abgelehnt wird", + "settings.sandboxing.title": "Sandbox", + "settings.sandboxing.network.title": "Netzwerkzugriff einschränken", + "settings.sandboxing.network.description": + "Blockiert den ausgehenden Netzwerkzugriff für vom Modell initiierte Befehle und HTTP-Tools. Lokale MCP-Server und Plugin-Hooks sind von dieser Einschränkung ausgenommen. Anbieter- und Modellinferenzdatenverkehr bleibt verfügbar.", "settings.experimental.mcpTimeout.title": "MCP-Zeitlimit (ms)", "settings.experimental.mcpTimeout.description": "Zeitlimit für MCP-Server-Anfragen in Millisekunden", "settings.experimental.remote.title": "Remote-Steuerung", @@ -1479,6 +1492,9 @@ export const dict = { "Keine benutzerdefinierten Befehle konfiguriert. Fügen Sie Befehle zu opencode.json hinzu, um sie hier zu sehen.", "settings.agentBehaviour.workflows.detail.description": "Beschreibung", "settings.agentBehaviour.workflows.detail.template": "Vorlage", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Shell-Befehle des Agenten in einer Sandbox auf Betriebssystemebene ausführen, die Schreibvorgänge auf die Projekt- und Kilo-Statusverzeichnisse beschränkt", "settings.autoApprove.description": "Legen Sie fest, wie Tools ausgeführt werden dürfen. Die meisten Tools sind standardmäßig auf Zulassen eingestellt. doom_loop und external_directory sind standardmäßig auf Fragen eingestellt.", @@ -1580,6 +1596,9 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Legacy-Migration", "settings.aboutKiloCode.legacyMigration.description": "Migrieren Sie Einstellungen von einer früheren Installation von Kilo Code, einschließlich Anbieter-API-Schlüsseln und dem Standardmodell.", + "settings.aboutKiloCode.rooImport.description": + "Importieren Sie den Konversationsverlauf von einer Installation von Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Sitzungen aus Roo Code importieren", // Screen 1 — What's New "migration.whatsNew.title": "Neuigkeiten in Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index d2805027db7..f3996d15811 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -283,6 +283,12 @@ export const dict = { "prompt.action.autoApprove.disable": "Disable auto-approve", "prompt.action.autoApprove.enabled": "Auto-approve is enabled. Permission prompts will be approved automatically.", "prompt.action.autoApprove.disabled": "Auto-approve is disabled. Click to approve permission prompts automatically.", + "prompt.action.sandbox.enable": "Enable sandbox", + "prompt.action.sandbox.disable": "Disable sandbox", + "prompt.action.sandbox.enabled": + "Sandbox is enabled. Agent shell commands are confined to the project and Kilo directories.", + "prompt.action.sandbox.disabled": + "Sandbox is disabled. Click to confine agent shell command writes to the project and Kilo directories.", "prompt.action.resetModel": "Reset model to default", "prompt.action.enhanceDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.", @@ -792,10 +798,10 @@ export const dict = { "settings.providers.tag.other": "Other", "settings.providers.connected.environmentDescription": "Connected from your environment variables", "settings.providers.action.signInChatGPT": "Sign in with ChatGPT", - "settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.", + "settings.providers.custom.description": "Add a custom provider by base URL.", "provider.custom.title": "Custom provider", - "provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ", + "provider.custom.description.prefix": "Configure a custom provider. See the ", "provider.custom.description.link": "provider config docs", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Provider ID", @@ -1081,6 +1087,9 @@ export const dict = { "settings.indexing.status.title": "Status", "settings.indexing.enable.title": "Enable indexing", "settings.indexing.enable.description": "Turn semantic codebase indexing on or off.", + "settings.indexing.showButton.title": "Show button when indexing is off", + "settings.indexing.showButton.description": + "Show the indexing button below the prompt while indexing is off. If hidden, open Settings > Indexing to enable indexing.", "settings.indexing.globalEnable.title": "Enable globally", "settings.indexing.globalEnable.description": "Enable indexing for every workspace.", "settings.indexing.projectEnable.title": "Enable for this project", @@ -1302,6 +1311,13 @@ export const dict = { "settings.models.speechToTextModel.description": "Choose the Kilo Gateway transcription model for voice input.", "settings.experimental.continueOnDeny.title": "Continue on Deny", "settings.experimental.continueOnDeny.description": "Continue the agent loop when a permission is denied", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Run agent shell commands inside an OS-level sandbox that restricts writes to the project and Kilo state directories", + "settings.sandboxing.title": "Sandboxing", + "settings.sandboxing.network.title": "Restrict Network Access", + "settings.sandboxing.network.description": + "Block outbound network access from model-originated commands and HTTP tools. Local MCP servers and plugin hooks run outside this restriction. Provider and model inference traffic remains available.", "settings.experimental.mcpTimeout.title": "MCP Timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout for MCP server requests in milliseconds", "settings.experimental.remote.title": "Remote Control", @@ -1527,7 +1543,7 @@ export const dict = { "settings.providers.enabled": "Enabled Providers (Allowlist)", "settings.providers.enabled.description": "If set, only these providers will be available (exclusive allowlist)", "settings.providers.notSet": "Not set (use server default)", - "settings.providers.custom.note": "Add an OpenAI-compatible provider by base URL.", + "settings.providers.custom.note": "Add a custom provider by base URL.", "settings.providers.search.placeholder": "Search providers", "settings.providers.select.placeholder": "Select provider...", @@ -1546,6 +1562,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Legacy Migration", "settings.aboutKiloCode.legacyMigration.description": "Migrate settings from a previous installation of Kilo Code, including provider API keys and default model.", + "settings.aboutKiloCode.rooImport.description": "Import conversation history from a Roo Code installation.", + "settings.aboutKiloCode.rooImport.button": "Import Sessions from Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "What's New in Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 8a00296c400..8ad90a35777 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -288,6 +288,12 @@ export const dict = { "prompt.action.enhanceDescription": "El botón 'Mejorar el mensaje' ayuda a mejorar tu petición proporcionando contexto adicional, aclaraciones o reformulaciones. Intenta escribir una petición aquí y haz clic en el botón nuevamente para ver cómo funciona.", "prompt.action.indexing": "Configuración de indexación", + "prompt.action.sandbox.enable": "Activar sandbox", + "prompt.action.sandbox.disable": "Desactivar sandbox", + "prompt.action.sandbox.enabled": + "Sandbox activado. Los comandos de shell del agente están limitados a los directorios del proyecto y de Kilo.", + "prompt.action.sandbox.disabled": + "Sandbox desactivado. Haz clic para limitar las escrituras de los comandos de shell del agente a los directorios del proyecto y de Kilo.", "speechToText.tooltip.start": "Iniciar entrada de voz con Kilo Gateway", "speechToText.tooltip.stop": "Detener captura", @@ -794,6 +800,9 @@ export const dict = { "settings.indexing.title": "Indexación", "settings.indexing.enable.title": "Habilitar indexación", "settings.indexing.enable.description": "Activar o desactivar la indexación semántica de la base de código.", + "settings.indexing.showButton.title": "Mostrar el botón cuando la indexación está desactivada", + "settings.indexing.showButton.description": + "Muestra el botón de indexación debajo del campo de entrada mientras la indexación esté desactivada. Si el botón está oculto, abre Configuración > Indexación para activar la indexación.", "settings.indexing.globalEnable.title": "Habilitar globalmente", "settings.indexing.globalEnable.description": "Habilitar la indexación para cada área de trabajo.", "settings.indexing.projectEnable.title": "Habilitar para este proyecto", @@ -866,7 +875,7 @@ export const dict = { "settings.providers.tag.customProvider": "Proveedor personalizado", "settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno", "settings.providers.action.signInChatGPT": "Iniciar sesión con ChatGPT", - "settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por URL base.", + "settings.providers.custom.description": "Añade un proveedor personalizado por URL base.", "settings.providers.subagentModel.title": "Modelo de subagente", "settings.providers.subagentModel.description": "Modelo predeterminado y esfuerzo de razonamiento para los subagentes de task-tool. Déjelo sin configurar para heredar el modelo del agente invocador.", @@ -874,11 +883,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Oculta los modelos de Kilo Gateway cuyos proveedores pueden usar tus prompts para entrenamiento.", "settings.providers.modeModels": "Modelo por modo", - "settings.providers.custom.note": "Agrega un proveedor compatible con OpenAI mediante URL base.", + "settings.providers.custom.note": "Agrega un proveedor personalizado mediante URL base.", "settings.providers.modeModels.description": "Anula el modelo predeterminado para modos específicos. Si no se establece, se usa el modelo predeterminado global.", "provider.custom.title": "Proveedor personalizado", - "provider.custom.description.prefix": "Configura un proveedor compatible con OpenAI. Consulta la ", + "provider.custom.description.prefix": "Configura un proveedor personalizado. Consulta la ", "provider.custom.description.link": "documentación de configuración de proveedores", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID del proveedor", @@ -1335,6 +1344,10 @@ export const dict = { "Elige el modelo de transcripción de Kilo Gateway para la entrada de voz.", "settings.experimental.continueOnDeny.title": "Continuar al denegar", "settings.experimental.continueOnDeny.description": "Continuar el bucle del agente cuando se deniega un permiso", + "settings.sandboxing.title": "Sandbox", + "settings.sandboxing.network.title": "Restringir el acceso a la red", + "settings.sandboxing.network.description": + "Bloquea el acceso saliente a la red para los comandos iniciados por el modelo y las herramientas HTTP. Los servidores MCP locales y los hooks de plugins no están sujetos a esta restricción. El tráfico de proveedores y de inferencia de modelos sigue estando disponible.", "settings.experimental.mcpTimeout.title": "Tiempo de espera MCP (ms)", "settings.experimental.mcpTimeout.description": "Tiempo de espera para solicitudes del servidor MCP en milisegundos", "settings.experimental.remote.title": "Control Remote", @@ -1471,6 +1484,9 @@ export const dict = { "No hay comandos personalizados configurados. Añada comandos a opencode.json para verlos aquí.", "settings.agentBehaviour.workflows.detail.description": "Descripción", "settings.agentBehaviour.workflows.detail.template": "Plantilla", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Ejecutar los comandos de shell del agente dentro de un sandbox a nivel de sistema operativo que restringe las escrituras a los directorios de estado del proyecto y de Kilo", "settings.autoApprove.description": "Defina cómo se permite la ejecución de las herramientas. La mayoría de las herramientas tienen como valor predeterminado Permitir. doom_loop y external_directory tienen como valor predeterminado Preguntar.", @@ -1572,6 +1588,9 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migración heredada", "settings.aboutKiloCode.legacyMigration.description": "Migra la configuración de una instalación anterior de Kilo Code, incluyendo las claves API de proveedores y el modelo predeterminado.", + "settings.aboutKiloCode.rooImport.description": + "Importa el historial de conversaciones de una instalación de Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importar sesiones desde Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Novedades en Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 3b2903b2d10..656e647fb7a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -289,6 +289,12 @@ export const dict = { "prompt.action.enhanceDescription": "Le bouton 'Améliorer la requête' aide à améliorer votre demande en fournissant un contexte supplémentaire, des clarifications ou des reformulations. Essayez de taper une demande ici et cliquez à nouveau sur le bouton pour voir comment cela fonctionne.", "prompt.action.indexing": "Paramètres d'indexation", + "prompt.action.sandbox.enable": "Activer le sandbox", + "prompt.action.sandbox.disable": "Désactiver le sandbox", + "prompt.action.sandbox.enabled": + "Le sandbox est activé. Les commandes shell de l'agent sont limitées aux répertoires du projet et de Kilo.", + "prompt.action.sandbox.disabled": + "Le sandbox est désactivé. Cliquez pour limiter les écritures des commandes shell de l'agent aux répertoires du projet et de Kilo.", "speechToText.tooltip.start": "Démarrer la saisie vocale avec Kilo Gateway", "speechToText.tooltip.stop": "Arrêter la capture audio", @@ -722,6 +728,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "Auto", "settings.indexing.dimension.title": "Dimension vectorielle", "settings.indexing.enable.description": "Activer ou désactiver l'indexation sémantique de la base de code.", + "settings.indexing.showButton.title": "Afficher le bouton lorsque l’indexation est désactivée", + "settings.indexing.showButton.description": + "Affichez le bouton d’indexation sous le champ de saisie lorsque l’indexation est désactivée. S’il est masqué, ouvrez Paramètres > Indexation pour activer l’indexation.", "settings.indexing.enable.title": "Activer l'indexation", "settings.indexing.globalEnable.title": "Activer globalement", "settings.indexing.globalEnable.description": "Activer l'indexation pour chaque espace de travail.", @@ -872,7 +881,7 @@ export const dict = { "settings.providers.tag.customProvider": "Fournisseur personnalisé", "settings.providers.connected.environmentDescription": "Connecté depuis vos variables d'environnement", "settings.providers.action.signInChatGPT": "Se connecter avec ChatGPT", - "settings.providers.custom.description": "Ajoutez un fournisseur compatible OpenAI par URL de base.", + "settings.providers.custom.description": "Ajoutez un fournisseur personnalisé par URL de base.", "settings.providers.subagentModel.title": "Modèle de sous-agent", "settings.providers.subagentModel.description": "Modèle par défaut et effort de raisonnement pour les sous-agents du task-tool. Laissez vide pour hériter du modèle de l'agent appelant.", @@ -880,11 +889,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Masquez les modèles Kilo Gateway dont les fournisseurs peuvent utiliser vos prompts à des fins d'entraînement.", "settings.providers.modeModels": "Modèle par mode", - "settings.providers.custom.note": "Ajoutez un fournisseur compatible OpenAI par URL de base.", + "settings.providers.custom.note": "Ajoutez un fournisseur personnalisé par URL de base.", "settings.providers.modeModels.description": "Remplacez le modèle par défaut pour des modes spécifiques. Si non défini, le modèle par défaut global est utilisé.", "provider.custom.title": "Fournisseur personnalisé", - "provider.custom.description.prefix": "Configurez un fournisseur compatible OpenAI. Voir la ", + "provider.custom.description.prefix": "Configurez un fournisseur personnalisé. Voir la ", "provider.custom.description.link": "documentation de configuration des fournisseurs", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID du fournisseur", @@ -1351,6 +1360,10 @@ export const dict = { "settings.experimental.continueOnDeny.title": "Continuer en cas de refus", "settings.experimental.continueOnDeny.description": "Continuer la boucle de l'agent lorsqu'une autorisation est refusée", + "settings.sandboxing.title": "Mise en bac à sable", + "settings.sandboxing.network.title": "Restreindre l'accès au réseau", + "settings.sandboxing.network.description": + "Bloquer l'accès réseau sortant des commandes provenant du modèle et des outils HTTP. Les serveurs MCP locaux et les hooks de plugin ne sont pas soumis à cette restriction. Le trafic d'inférence des fournisseurs et des modèles reste disponible.", "settings.experimental.mcpTimeout.title": "Délai MCP (ms)", "settings.experimental.mcpTimeout.description": "Délai des requêtes du serveur MCP en millisecondes", "settings.experimental.remote.title": "Contrôle Remote", @@ -1488,6 +1501,9 @@ export const dict = { "Aucune commande personnalisée configurée. Ajoutez des commandes à opencode.json pour les voir ici.", "settings.agentBehaviour.workflows.detail.description": "Description", "settings.agentBehaviour.workflows.detail.template": "Modèle", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Exécuter les commandes shell de l'agent dans un sandbox au niveau du système d'exploitation qui restreint les écritures aux répertoires d'état du projet et de Kilo", "settings.autoApprove.description": "Définissez comment les outils sont autorisés à s'exécuter. La plupart des outils sont définis sur Autoriser par défaut. doom_loop et external_directory sont définis sur Demander par défaut.", @@ -1592,6 +1608,9 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migration de l'ancienne version", "settings.aboutKiloCode.legacyMigration.description": "Migrer les paramètres d'une précédente installation de Kilo Code, y compris les clés d'API des fournisseurs et le modèle par défaut.", + "settings.aboutKiloCode.rooImport.description": + "Importer l'historique des conversations depuis une installation de Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importer les sessions depuis Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Nouveautés de Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 3b0e8a41ed5..9ae8414475c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -689,9 +689,9 @@ export const dict = { "settings.providers.tag.other": "Altro", "settings.providers.connected.environmentDescription": "Connesso dalle variabili d'ambiente", "settings.providers.action.signInChatGPT": "Accedi con ChatGPT", - "settings.providers.custom.description": "Aggiungi un provider compatibile con OpenAI tramite URL base.", + "settings.providers.custom.description": "Aggiungi un provider personalizzato tramite URL base.", "provider.custom.title": "Provider personalizzato", - "provider.custom.description.prefix": "Configura un provider compatibile con OpenAI. Vedi la ", + "provider.custom.description.prefix": "Configura un provider personalizzato. Vedi la ", "provider.custom.description.link": "documentazione di configurazione provider", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID provider", @@ -970,6 +970,9 @@ export const dict = { "settings.indexing.status.title": "Stato", "settings.indexing.enable.title": "Abilita indicizzazione", "settings.indexing.enable.description": "Attiva o disattiva l'indicizzazione semantica del codebase.", + "settings.indexing.showButton.title": "Mostra il pulsante quando l'indicizzazione è disattivata", + "settings.indexing.showButton.description": + "Mostra il pulsante di indicizzazione sotto il prompt quando l'indicizzazione è disattivata. Se il pulsante è nascosto, apri Impostazioni > Indicizzazione per attivare l'indicizzazione.", "settings.indexing.provider.title": "Provider embedding", "settings.indexing.provider.description": "Scegli il provider usato per generare embedding per la ricerca semantica.", "settings.indexing.model.title": "Modello embedding", @@ -1158,6 +1161,10 @@ export const dict = { "settings.experimental.codebaseSearch.description": "Abilita ricerca in linguaggio naturale con AI nel codebase", "settings.experimental.continueOnDeny.title": "Continua dopo rifiuto", "settings.experimental.continueOnDeny.description": "Continua il loop agente quando un'autorizzazione viene negata", + "settings.sandboxing.title": "Sandbox", + "settings.sandboxing.network.title": "Limita l'accesso alla rete", + "settings.sandboxing.network.description": + "Blocca l'accesso in uscita alla rete per i comandi avviati dal modello e gli strumenti HTTP. I server MCP locali e gli hook dei plugin operano al di fuori di questa restrizione. Il traffico verso i provider e per l'inferenza dei modelli rimane disponibile.", "settings.experimental.mcpTimeout.title": "Timeout MCP (ms)", "settings.experimental.mcpTimeout.description": "Timeout per richieste server MCP in millisecondi", "settings.experimental.remote.title": "Controllo remoto", @@ -1197,6 +1204,15 @@ export const dict = { "settings.agentBehaviour.noSkillsFound": "Nessuna skill rilevata. Aggiungi percorsi cartella o URL di skill qui sotto per renderle disponibili.", "settings.agentBehaviour.availableModes": "Modalità personalizzate disponibili", + "prompt.action.sandbox.enable": "Abilita sandbox", + "prompt.action.sandbox.disable": "Disabilita sandbox", + "prompt.action.sandbox.enabled": + "Sandbox abilitata. I comandi shell dell'agente sono limitati alle directory del progetto e di Kilo.", + "prompt.action.sandbox.disabled": + "Sandbox disabilitata. Fai clic per limitare le scritture dei comandi shell dell'agente alle directory del progetto e di Kilo.", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Esegui i comandi shell dell'agente all'interno di un sandbox a livello di sistema operativo che limita le scritture alle directory di stato del progetto e di Kilo", "settings.agentBehaviour.skillPaths": "Percorsi cartelle skill", "settings.agentBehaviour.skillUrls": "URL skill", @@ -1371,7 +1387,7 @@ export const dict = { "settings.providers.enabled.description": "Se impostato, saranno disponibili solo questi provider (allowlist esclusiva)", "settings.providers.notSet": "Non impostato (usa default server)", - "settings.providers.custom.note": "Aggiungi un provider compatibile con OpenAI tramite URL base.", + "settings.providers.custom.note": "Aggiungi un provider personalizzato tramite URL base.", "settings.providers.search.placeholder": "Cerca provider", "settings.providers.select.placeholder": "Seleziona provider...", "dialog.model.notSet": "Non impostato", @@ -1383,6 +1399,9 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migrazione legacy", "settings.aboutKiloCode.legacyMigration.description": "Migra le impostazioni da una precedente installazione di Kilo Code, incluse API key dei provider e modello predefinito.", + "settings.aboutKiloCode.rooImport.description": + "Importa la cronologia delle conversazioni da una installazione di Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importa sessioni da Roo Code", "migration.whatsNew.title": "Novità in Kilo Code", "migration.whatsNew.subtitle": "Abbiamo ricostruito l'estensione su fondamenta più veloci ed efficienti.", "migration.whatsNew.features.performance.title": "Prestazioni agente più rapide", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 05373402a49..9ec27bf7036 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -284,6 +284,12 @@ export const dict = { "prompt.action.enhanceDescription": "「プロンプトを強化」ボタンは、追加コンテキスト、説明、または言い換えを提供することで、リクエストを改善します。ここにリクエストを入力し、ボタンを再度クリックして動作を確認してください。", "prompt.action.indexing": "インデックス設定", + "prompt.action.sandbox.enable": "サンドボックスを有効化", + "prompt.action.sandbox.disable": "サンドボックスを無効化", + "prompt.action.sandbox.enabled": + "サンドボックスが有効です。エージェントのシェルコマンドは、プロジェクトおよびKiloディレクトリに制限されています。", + "prompt.action.sandbox.disabled": + "サンドボックスが無効です。クリックしてエージェントのシェルコマンドの書き込みをプロジェクトおよびKiloディレクトリに制限します。", "speechToText.tooltip.start": "Kilo Gatewayで音声入力を開始", "speechToText.tooltip.stop": "音声キャプチャを停止", @@ -787,6 +793,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "自動", "settings.indexing.dimension.title": "ベクトル次元", "settings.indexing.enable.description": "セマンティックコードベースインデックスをオンまたはオフにします。", + "settings.indexing.showButton.title": "インデックス作成がオフのときにボタンを表示", + "settings.indexing.showButton.description": + "インデックス作成がオフの間、プロンプトの下にインデックス作成ボタンを表示します。ボタンが非表示の場合は、設定 > インデックス作成を開いてインデックス作成を有効にしてください。", "settings.indexing.enable.title": "インデックスを有効にする", "settings.indexing.globalEnable.title": "グローバルで有効にする", "settings.indexing.globalEnable.description": "すべてのワークスペースでインデックス作成を有効にします。", @@ -854,7 +863,7 @@ export const dict = { "settings.providers.tag.customProvider": "カスタムプロバイダー", "settings.providers.connected.environmentDescription": "環境変数から接続されています", "settings.providers.action.signInChatGPT": "ChatGPT でサインイン", - "settings.providers.custom.description": "ベースURLでOpenAI互換プロバイダーを追加します。", + "settings.providers.custom.description": "ベース URL でカスタムプロバイダーを追加します。", "settings.providers.subagentModel.title": "サブエージェントモデル", "settings.providers.subagentModel.description": "task-tool サブエージェントのデフォルトモデルと推論の労力。呼び出し元のエージェントのモデルを継承する場合は未設定のままにしてください。", @@ -862,11 +871,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "プロバイダーがプロンプトを学習に使用する可能性のある Kilo Gateway モデルを非表示にします。", "settings.providers.modeModels": "モードごとのモデル", - "settings.providers.custom.note": "Base URL で OpenAI 互換プロバイダーを追加します。", + "settings.providers.custom.note": "Base URL でカスタムプロバイダーを追加します。", "settings.providers.modeModels.description": "特定のモードのデフォルトモデルを上書きします。設定されていない場合、グローバルデフォルトモデルが使用されます。", "provider.custom.title": "カスタムプロバイダー", - "provider.custom.description.prefix": "OpenAI互換プロバイダーを設定します。", + "provider.custom.description.prefix": "カスタムプロバイダーを設定します。", "provider.custom.description.link": "プロバイダー設定ドキュメント", "provider.custom.description.suffix": "を参照してください。", "provider.custom.field.providerID.label": "プロバイダーID", @@ -1313,6 +1322,10 @@ export const dict = { "settings.models.speechToTextModel.description": "音声入力に使用するKilo Gateway文字起こしモデルを選択します。", "settings.experimental.continueOnDeny.title": "拒否時に続行", "settings.experimental.continueOnDeny.description": "権限が拒否された場合にエージェントループを続行", + "settings.sandboxing.title": "サンドボックス化", + "settings.sandboxing.network.title": "ネットワークアクセスを制限", + "settings.sandboxing.network.description": + "モデルから発行されたコマンドと HTTP ツールによる外部ネットワークアクセスをブロックします。ローカル MCP サーバーとプラグインフックは、この制限の対象外です。プロバイダーおよびモデルへの推論通信は引き続き利用できます。", "settings.experimental.mcpTimeout.title": "MCPタイムアウト(ミリ秒)", "settings.experimental.mcpTimeout.description": "MCPサーバーリクエストのタイムアウト(ミリ秒)", "settings.experimental.remote.title": "Remote コントロール", @@ -1446,6 +1459,9 @@ export const dict = { "カスタムコマンドが設定されていません。opencode.json にコマンドを追加するとここに表示されます。", "settings.agentBehaviour.workflows.detail.description": "説明", "settings.agentBehaviour.workflows.detail.template": "テンプレート", + "settings.experimental.sandbox.title": "サンドボックス", + "settings.experimental.sandbox.description": + "エージェントのシェルコマンドを、プロジェクトおよびKiloの状態ディレクトリへの書き込みを制限するOSレベルのサンドボックス内で実行", "settings.autoApprove.description": "ツールの実行許可を定義します。ほとんどのツールはデフォルトで「許可」されます。doom_loop と external_directory はデフォルトで「確認」になります。", @@ -1543,6 +1559,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "レガシー移行", "settings.aboutKiloCode.legacyMigration.description": "プロバイダーのAPIキーやデフォルトモデルなど、以前にインストールしたKilo Codeから設定を移行します。", + "settings.aboutKiloCode.rooImport.description": "インストールしたRoo Codeから会話履歴を移行します。", + "settings.aboutKiloCode.rooImport.button": "Roo Codeからセッションをインポート", // Screen 1 — What's New "migration.whatsNew.title": "Kilo Codeの新機能", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index e22fe7fd9a8..a26e3306f6c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -286,6 +286,12 @@ export const dict = { "prompt.action.resetModel": "모델을 기본값으로 재설정", "prompt.action.enhanceDescription": "'프롬프트 향상' 버튼은 추가 컨텍스트, 명확화 또는 재구성을 제공하여 요청을 개선합니다. 여기에 요청을 입력한 다음 버튼을 다시 클릭하여 작동 방식을 확인해보세요.", + "prompt.action.sandbox.enable": "샌드박스 활성화", + "prompt.action.sandbox.disable": "샌드박스 비활성화", + "prompt.action.sandbox.enabled": + "샌드박스가 활성화되어 있습니다. 에이전트 셸 명령은 프로젝트 및 Kilo 디렉터리로 제한됩니다.", + "prompt.action.sandbox.disabled": + "샌드박스가 비활성화되어 있습니다. 클릭하여 에이전트 셸 명령 쓰기를 프로젝트 및 Kilo 디렉터리로 제한합니다.", "speechToText.tooltip.start": "Kilo Gateway로 음성 입력 시작", "speechToText.tooltip.stop": "음성 캡처 중지", @@ -816,7 +822,7 @@ export const dict = { "settings.providers.tag.customProvider": "사용자 정의 공급자", "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨", "settings.providers.action.signInChatGPT": "ChatGPT로 로그인", - "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.", + "settings.providers.custom.description": "기본 URL로 사용자 정의 공급자를 추가합니다.", "settings.providers.subagentModel.title": "하위 에이전트 모델", "settings.providers.subagentModel.description": "task-tool 하위 에이전트의 기본 모델 및 추론 수준입니다. 호출하는 에이전트의 모델을 상속하려면 비워 두세요.", @@ -824,11 +830,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "제공업체가 사용자의 프롬프트를 학습에 사용할 수 있는 Kilo Gateway 모델을 숨깁니다.", "settings.providers.modeModels": "모드별 모델", - "settings.providers.custom.note": "Base URL로 OpenAI 호환 공급자를 추가합니다.", + "settings.providers.custom.note": "Base URL로 사용자 정의 공급자를 추가합니다.", "settings.providers.modeModels.description": "특정 모드의 기본 모델을 재정의합니다. 설정하지 않으면 전역 기본 모델이 사용됩니다.", "provider.custom.title": "사용자 정의 공급자", - "provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ", + "provider.custom.description.prefix": "사용자 정의 공급자를 구성합니다. ", "provider.custom.description.link": "공급자 구성 문서", "provider.custom.description.suffix": "를 참조하세요.", "provider.custom.field.providerID.label": "공급자 ID", @@ -1122,6 +1128,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "자동", "settings.indexing.dimension.title": "벡터 차원", "settings.indexing.enable.description": "의미적 코드베이스 인덱싱을 켜거나 끕니다.", + "settings.indexing.showButton.title": "인덱싱이 꺼져 있을 때 버튼 표시", + "settings.indexing.showButton.description": + "인덱싱이 꺼져 있는 동안 프롬프트 아래에 인덱싱 버튼을 표시합니다. 버튼이 숨겨져 있는 경우 설정 > 인덱싱을 열어 인덱싱을 활성화하세요.", "settings.indexing.enable.title": "인덱싱 활성화", "settings.indexing.globalEnable.title": "전역으로 활성화", "settings.indexing.globalEnable.description": "모든 작업 영역에 대해 인덱싱을 활성화합니다.", @@ -1307,6 +1316,10 @@ export const dict = { "settings.models.speechToTextModel.description": "음성 입력에 사용할 Kilo Gateway 변환 모델을 선택하세요.", "settings.experimental.continueOnDeny.title": "거부 시 계속", "settings.experimental.continueOnDeny.description": "권한이 거부되면 에이전트 루프 계속", + "settings.sandboxing.title": "샌드박스", + "settings.sandboxing.network.title": "네트워크 액세스 제한", + "settings.sandboxing.network.description": + "모델이 실행한 명령과 HTTP 도구의 아웃바운드 네트워크 액세스를 차단합니다. 로컬 MCP 서버와 플러그인 훅에는 이 제한이 적용되지 않습니다. 공급자 및 모델 추론 트래픽은 계속 사용할 수 있습니다.", "settings.experimental.mcpTimeout.title": "MCP 타임아웃 (ms)", "settings.experimental.mcpTimeout.description": "MCP 서버 요청의 타임아웃 시간 (밀리초)", "settings.experimental.remote.title": "Remote 제어", @@ -1436,6 +1449,9 @@ export const dict = { "구성된 사용자 정의 명령이 없습니다. opencode.json에 명령을 추가하면 여기에 표시됩니다.", "settings.agentBehaviour.workflows.detail.description": "설명", "settings.agentBehaviour.workflows.detail.template": "템플릿", + "settings.experimental.sandbox.title": "샌드박스", + "settings.experimental.sandbox.description": + "에이전트 셸 명령을 프로젝트 및 Kilo 상태 디렉터리에 대한 쓰기를 제한하는 OS 수준의 샌드박스 내에서 실행", "settings.autoApprove.description": "도구 실행 허용 방식을 정의합니다. 대부분의 도구 기본값은 '허용'입니다. doom_loop 및 external_directory의 기본값은 '확인'입니다.", @@ -1527,6 +1543,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "레거시 마이그레이션", "settings.aboutKiloCode.legacyMigration.description": "공급자 API 키 및 기본 모델을 포함하여 이전 Kilo Code 설치에서 설정을 마이그레이션합니다.", + "settings.aboutKiloCode.rooImport.description": "Roo Code 설치에서 대화 기록을 가져옵니다.", + "settings.aboutKiloCode.rooImport.button": "Roo Code에서 세션 가져오기", // Screen 1 — What's New "migration.whatsNew.title": "Kilo Code의 새로운 기능", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index d5a65ac96bf..ad3a5a966d8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -288,6 +288,12 @@ export const dict = { "prompt.action.resetModel": "Model terugzetten naar standaard", "prompt.action.enhanceDescription": "De knop 'Prompt verbeteren' helpt je prompt te verbeteren door extra context, verduidelijking of herformulering te bieden. Typ hier een prompt en klik nogmaals op de knop om te zien hoe het werkt.", + "prompt.action.sandbox.enable": "Sandbox inschakelen", + "prompt.action.sandbox.disable": "Sandbox uitschakelen", + "prompt.action.sandbox.enabled": + "Sandbox is ingeschakeld. Shell-opdrachten van de agent zijn beperkt tot de project- en Kilo-mappen.", + "prompt.action.sandbox.disabled": + "Sandbox is uitgeschakeld. Klik om schrijfbewerkingen van shell-opdrachten van de agent te beperken tot de project- en Kilo-mappen.", "speechToText.tooltip.start": "Spraakinvoer starten met Kilo Gateway", "speechToText.tooltip.stop": "Audio vastleggen stoppen", @@ -826,10 +832,10 @@ export const dict = { "settings.providers.tag.other": "Overige", "settings.providers.connected.environmentDescription": "Gekoppeld via je omgevingsvariabelen", "settings.providers.action.signInChatGPT": "Inloggen met ChatGPT", - "settings.providers.custom.description": "Voeg een OpenAI-compatibele provider toe via basis-URL.", + "settings.providers.custom.description": "Voeg een aangepaste provider toe via basis-URL.", "provider.custom.title": "Aangepaste provider", - "provider.custom.description.prefix": "Configureer een OpenAI-compatibele provider. Zie de ", + "provider.custom.description.prefix": "Configureer een aangepaste provider. Zie de ", "provider.custom.description.link": "provider configuratie documentatie", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Provider-ID", @@ -1122,6 +1128,9 @@ export const dict = { "settings.indexing.status.title": "Status", "settings.indexing.enable.title": "Indexering inschakelen", "settings.indexing.enable.description": "Schakel semantische codebase-indexering in of uit.", + "settings.indexing.showButton.title": "Knop weergeven wanneer indexering is uitgeschakeld", + "settings.indexing.showButton.description": + "Geef de indexeringsknop onder de prompt weer wanneer indexering is uitgeschakeld. Als de knop verborgen is, opent u Instellingen > Indexering om indexering in te schakelen.", "settings.indexing.globalEnable.title": "Globaal inschakelen", "settings.indexing.globalEnable.description": "Indexering inschakelen voor elke werkruimte.", "settings.indexing.projectEnable.title": "Inschakelen voor dit project", @@ -1321,6 +1330,10 @@ export const dict = { "settings.experimental.continueOnDeny.title": "Doorgaan bij weigering", "settings.experimental.continueOnDeny.description": "Ga door met de agent loop wanneer een toestemming wordt geweigerd", + "settings.sandboxing.title": "Sandbox", + "settings.sandboxing.network.title": "Netwerktoegang beperken", + "settings.sandboxing.network.description": + "Blokkeer uitgaande netwerktoegang voor door het model geïnitieerde opdrachten en HTTP-tools. Lokale MCP-servers en plugin-hooks vallen buiten deze beperking. Netwerkverkeer voor providers en modelinferentie blijft beschikbaar.", "settings.experimental.mcpTimeout.title": "MCP Timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout voor MCP-serververzoeken in milliseconden", "settings.experimental.remote.title": "Remote-bediening", @@ -1332,6 +1345,9 @@ export const dict = { "settings.experimental.remote.inactive": "Inactief", "settings.experimental.remote.hint": "Gebruik /remote in de chat om te schakelen", "settings.experimental.toolToggles": "Tool Schakelaars", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Shell-opdrachten van de agent uitvoeren in een sandbox op besturingssysteemniveau die schrijfbewerkingen beperkt tot de project- en Kilo-statusmappen", "settings.agentBehaviour.defaultAgent.title": "Standaard Agent", "settings.agentBehaviour.defaultAgent.description": "Agent om te gebruiken wanneer er geen is opgegeven", @@ -1521,7 +1537,7 @@ export const dict = { "settings.providers.enabled.description": "Indien ingesteld, zijn alleen deze providers beschikbaar (exclusieve allowlist)", "settings.providers.notSet": "Niet ingesteld (gebruik server standaard)", - "settings.providers.custom.note": "Voeg een OpenAI-compatibele provider toe via base URL.", + "settings.providers.custom.note": "Voeg een aangepaste provider toe via base URL.", "settings.providers.search.placeholder": "Zoek providers", "settings.providers.select.placeholder": "Selecteer provider...", @@ -1540,6 +1556,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Legacy Migratie", "settings.aboutKiloCode.legacyMigration.description": "Migreer instellingen van een eerdere installatie van Kilo Code, inclusief provider API-sleutels en standaard model.", + "settings.aboutKiloCode.rooImport.description": "Importeer gespreksgeschiedenis van een Roo Code installatie.", + "settings.aboutKiloCode.rooImport.button": "Sessies importeren uit Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Wat is er Nieuw in Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 9fd3853d353..fddcc85e4bf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -290,6 +290,12 @@ export const dict = { "prompt.action.indexing": "Indekseringsinnstillinger", "prompt.action.enhanceDescription": "Knappen 'Forbedre prompt' hjelper med å forbedre forespørselen din ved å gi ekstra kontekst, avklaring eller omformulering. Prøv å skrive en forespørsel her og klikk på knappen igjen for å se hvordan det fungerer.", + "prompt.action.sandbox.enable": "Aktiver sandbox", + "prompt.action.sandbox.disable": "Deaktiver sandbox", + "prompt.action.sandbox.enabled": + "Sandbox er aktivert. Shell-kommandoer for agenten er begrenset til prosjekt- og Kilo-mappene.", + "prompt.action.sandbox.disabled": + "Sandbox er deaktivert. Klikk for å begrense skriving av shell-kommandoer for agenten til prosjekt- og Kilo-mappene.", "speechToText.tooltip.start": "Start taleinndata med Kilo Gateway", "speechToText.tooltip.stop": "Stopp lydfangst", @@ -825,7 +831,7 @@ export const dict = { "settings.providers.tag.customProvider": "Egendefinert leverandør", "settings.providers.connected.environmentDescription": "Koblet til fra dine miljøvariabler", "settings.providers.action.signInChatGPT": "Logg inn med ChatGPT", - "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.", + "settings.providers.custom.description": "Legg til en egendefinert leverandør via basis-URL.", "settings.providers.subagentModel.title": "Underagentmodell", "settings.providers.subagentModel.description": "Standardmodell og resonneringsinnsats for task-tool-underagenter. La stå tom for å arve den kallende agentens modell.", @@ -833,11 +839,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Skjul Kilo Gateway-modeller der leverandørene kan bruke ledetekstene dine til trening.", "settings.providers.modeModels": "Modell per modus", - "settings.providers.custom.note": "Legg til en OpenAI-kompatibel leverandør via basis-URL.", + "settings.providers.custom.note": "Legg til en egendefinert leverandør via basis-URL.", "settings.providers.modeModels.description": "Overstyr standardmodellen for bestemte moduser. Hvis ikke angitt, brukes den globale standardmodellen.", "provider.custom.title": "Egendefinert leverandør", - "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ", + "provider.custom.description.prefix": "Konfigurer en egendefinert leverandør. Se ", "provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Leverandør-ID", @@ -1284,6 +1290,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Velg Kilo Gateway-transkripsjonsmodellen for taleinndata.", "settings.experimental.continueOnDeny.title": "Fortsett ved avvisning", "settings.experimental.continueOnDeny.description": "Fortsett agentløkken når en tillatelse avvises", + "settings.sandboxing.title": "Kjøring i sandkasse", + "settings.sandboxing.network.title": "Begrens nettverkstilgang", + "settings.sandboxing.network.description": + "Blokker utgående nettverkstilgang fra kommandoer generert av modellen og HTTP-verktøy. Lokale MCP-servere og programtilleggskroker kjører utenfor denne begrensningen. Trafikk for leverandør- og modellinferens forblir tilgjengelig.", "settings.experimental.mcpTimeout.title": "MCP-tidsavbrudd (ms)", "settings.experimental.mcpTimeout.description": "Tidsavbrudd for MCP-serverforespørsler i millisekunder", "settings.experimental.remote.title": "Remote-kontroll", @@ -1298,6 +1308,9 @@ export const dict = { "settings.indexing.title": "Indeksering", "settings.indexing.enable.title": "Aktiver indeksering", "settings.indexing.enable.description": "Slå semantisk kodebaseindeksering på eller av.", + "settings.indexing.showButton.title": "Vis knappen når indeksering er slått av", + "settings.indexing.showButton.description": + "Vis indekseringsknappen under ledeteksten mens indeksering er slått av. Hvis knappen er skjult, åpner du Innstillinger > Indeksering for å aktivere indeksering.", "settings.indexing.globalEnable.title": "Aktiver globalt", "settings.indexing.globalEnable.description": "Aktiver indeksering for hvert arbeidsområde.", "settings.indexing.projectEnable.title": "Aktiver for dette prosjektet", @@ -1449,6 +1462,9 @@ export const dict = { "Ingen egendefinerte kommandoer konfigurert. Legg til kommandoer i opencode.json for å se dem her.", "settings.agentBehaviour.workflows.detail.description": "Beskrivelse", "settings.agentBehaviour.workflows.detail.template": "Mal", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Kjør shell-kommandoer for agenten i en sandbox på operativsystemnivå som begrenser skriving til prosjekt- og Kilo-tilstandsmapper", "settings.autoApprove.description": "Definer hvordan verktøy kan kjøre. De fleste verktøy har Tillat som standard. doom_loop og external_directory har Spør som standard.", @@ -1546,6 +1562,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migrering fra eldre versjon", "settings.aboutKiloCode.legacyMigration.description": "Migrer innstillinger fra en tidligere installasjon av Kilo Code, inkludert API-nøkler for leverandører og standardmodell.", + "settings.aboutKiloCode.rooImport.description": "Importer samtalehistorikk fra en installasjon av Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importer økter fra Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Hva er nytt i Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 52f4c4faf59..f7db2440724 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -287,6 +287,12 @@ export const dict = { "prompt.action.enhanceDescription": "Przycisk 'Ulepsz podpowiedź' pomaga ulepszyć Twoją prośbę, dostarczając dodatkowy kontekst, wyjaśnienia lub przeformułowania. Spróbuj wpisać prośbę tutaj i kliknij przycisk ponownie, aby zobaczyć, jak to działa.", "prompt.action.indexing": "Ustawienia indeksowania", + "prompt.action.sandbox.enable": "Włącz sandbox", + "prompt.action.sandbox.disable": "Wyłącz sandbox", + "prompt.action.sandbox.enabled": + "Sandbox jest włączony. Polecenia shell agenta są ograniczone do katalogów projektu i Kilo.", + "prompt.action.sandbox.disabled": + "Sandbox jest wyłączony. Kliknij, aby ograniczyć zapisy poleceń shell agenta do katalogów projektu i Kilo.", "speechToText.tooltip.start": "Rozpocznij wprowadzanie głosowe z Kilo Gateway", "speechToText.tooltip.stop": "Zatrzymaj przechwytywanie dźwięku", @@ -823,7 +829,7 @@ export const dict = { "settings.providers.tag.customProvider": "Niestandardowy dostawca", "settings.providers.connected.environmentDescription": "Połączony z twoich zmiennych środowiskowych", "settings.providers.action.signInChatGPT": "Zaloguj przez ChatGPT", - "settings.providers.custom.description": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.", + "settings.providers.custom.description": "Dodaj niestandardowego dostawcę przez bazowy URL.", "settings.providers.subagentModel.title": "Model podagenta", "settings.providers.subagentModel.description": "Domyślny model i wysiłek wnioskowania dla podagentów task-tool. Pozostaw puste, aby odziedziczyć model agenta wywołującego.", @@ -831,11 +837,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Ukryj modele Kilo Gateway, których dostawcy mogą używać Twoich promptów do trenowania.", "settings.providers.modeModels": "Model na tryb", - "settings.providers.custom.note": "Dodaj dostawcę kompatybilnego z OpenAI przez bazowy URL.", + "settings.providers.custom.note": "Dodaj niestandardowego dostawcę przez bazowy URL.", "settings.providers.modeModels.description": "Zastąp domyślny model dla określonych trybów. Jeśli nie ustawiono, używany jest globalny domyślny model.", "provider.custom.title": "Niestandardowy dostawca", - "provider.custom.description.prefix": "Skonfiguruj dostawcę kompatybilnego z OpenAI. Zobacz ", + "provider.custom.description.prefix": "Skonfiguruj niestandardowego dostawcę. Zobacz ", "provider.custom.description.link": "dokumentację konfiguracji dostawcy", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID dostawcy", @@ -1285,6 +1291,10 @@ export const dict = { "Wybierz model transkrypcji Kilo Gateway dla wprowadzania głosowego.", "settings.experimental.continueOnDeny.title": "Kontynuuj przy odmowie", "settings.experimental.continueOnDeny.description": "Kontynuuj pętlę agenta po odmowie uprawnienia", + "settings.sandboxing.title": "Izolacja w piaskownicy", + "settings.sandboxing.network.title": "Ogranicz dostęp do sieci", + "settings.sandboxing.network.description": + "Blokuj wychodzący dostęp do sieci z poleceń pochodzących od modelu i narzędzi HTTP. Lokalne serwery MCP i hooki wtyczek nie podlegają temu ograniczeniu. Ruch do dostawców i modeli na potrzeby wnioskowania pozostaje dostępny.", "settings.experimental.mcpTimeout.title": "Limit czasu MCP (ms)", "settings.experimental.mcpTimeout.description": "Limit czasu żądań serwera MCP w milisekundach", "settings.experimental.remote.title": "Sterowanie Remote", @@ -1301,6 +1311,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "Automatycznie", "settings.indexing.dimension.title": "Wymiar wektora", "settings.indexing.enable.description": "Włącz lub wyłącz semantyczne indeksowanie bazy kodu.", + "settings.indexing.showButton.title": "Pokazuj przycisk, gdy indeksowanie jest wyłączone", + "settings.indexing.showButton.description": + "Pokazuj przycisk indeksowania pod monitem, gdy indeksowanie jest wyłączone. Jeśli przycisk jest ukryty, otwórz Ustawienia > Indeksowanie, aby włączyć indeksowanie.", "settings.indexing.enable.title": "Włącz indeksowanie", "settings.indexing.globalEnable.title": "Włącz globalnie", "settings.indexing.globalEnable.description": "Włącz indeksowanie dla każdego obszaru roboczego.", @@ -1454,6 +1467,9 @@ export const dict = { "Brak skonfigurowanych niestandardowych komend. Dodaj komendy do opencode.json, aby je tu zobaczyć.", "settings.agentBehaviour.workflows.detail.description": "Opis", "settings.agentBehaviour.workflows.detail.template": "Szablon", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Uruchamiaj polecenia shell agenta w sandboxie na poziomie systemu operacyjnego, który ogranicza zapisy do katalogów stanu projektu i Kilo", "settings.autoApprove.description": "Zdefiniuj, jak narzędzia mogą być uruchamiane. Większość narzędzi domyślnie ma ustawienie Zezwalaj. doom_loop i external_directory domyślnie mają ustawienie Pytaj.", @@ -1556,6 +1572,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Migracja starszej wersji", "settings.aboutKiloCode.legacyMigration.description": "Migruj ustawienia z poprzedniej instalacji Kilo Code, w tym klucze API dostawców i domyślny model.", + "settings.aboutKiloCode.rooImport.description": "Importuj historię rozmów z instalacji Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Importuj sesje z Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Co nowego w Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 9714096ec06..6990d1f7e79 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -285,6 +285,11 @@ export const dict = { "prompt.action.indexing": "Настройки индексации", "prompt.action.enhanceDescription": "Кнопка 'Улучшить запрос' помогает сделать ваш запрос лучше, предоставляя дополнительный контекст, уточнения или переформулировку. Попробуйте ввести запрос и снова нажать кнопку, чтобы увидеть, как это работает.", + "prompt.action.sandbox.enable": "Включить песочницу", + "prompt.action.sandbox.disable": "Отключить песочницу", + "prompt.action.sandbox.enabled": "Песочница включена. Команды оболочки агента ограничены каталогами проекта и Kilo.", + "prompt.action.sandbox.disabled": + "Песочница отключена. Нажмите, чтобы ограничить запись команд оболочки агента каталогами проекта и Kilo.", "speechToText.tooltip.start": "Начать голосовой ввод с Kilo Gateway", "speechToText.tooltip.stop": "Остановить захват звука", @@ -793,6 +798,9 @@ export const dict = { "settings.indexing.title": "Индексация", "settings.indexing.enable.title": "Включить индексацию", "settings.indexing.enable.description": "Включить или отключить семантическую индексацию кодовой базы.", + "settings.indexing.showButton.title": "Показывать кнопку, когда индексация отключена", + "settings.indexing.showButton.description": + "Показывать кнопку индексации под полем ввода, пока индексация отключена. Если кнопка скрыта, откройте «Настройки > Индексация», чтобы включить индексацию.", "settings.indexing.globalEnable.title": "Включить глобально", "settings.indexing.globalEnable.description": "Включить индексирование для каждого рабочего пространства.", "settings.indexing.projectEnable.title": "Включить для этого проекта", @@ -862,7 +870,7 @@ export const dict = { "settings.providers.tag.customProvider": "Пользовательский провайдер", "settings.providers.connected.environmentDescription": "Подключён из ваших переменных окружения", "settings.providers.action.signInChatGPT": "Войти через ChatGPT", - "settings.providers.custom.description": "Добавьте OpenAI-совместимый провайдер по базовому URL.", + "settings.providers.custom.description": "Добавьте пользовательский провайдер по базовому URL.", "settings.providers.subagentModel.title": "Модель субагента", "settings.providers.subagentModel.description": "Модель по умолчанию и уровень рассуждения для субагентов task-tool. Оставьте пустым, чтобы унаследовать модель вызывающего агента.", @@ -870,11 +878,11 @@ export const dict = { "settings.models.hidePromptTraining.description": "Скрывать модели Kilo Gateway, поставщики которых могут использовать ваши запросы для обучения.", "settings.providers.modeModels": "Модель для режима", - "settings.providers.custom.note": "Добавьте OpenAI-совместимого провайдера по базовому URL.", + "settings.providers.custom.note": "Добавьте пользовательского провайдера по базовому URL.", "settings.providers.modeModels.description": "Переопределите модель по умолчанию для определённых режимов. Если не задано, используется глобальная модель по умолчанию.", "provider.custom.title": "Пользовательский провайдер", - "provider.custom.description.prefix": "Настройте провайдер, совместимый с OpenAI. См. ", + "provider.custom.description.prefix": "Настройте пользовательский провайдер. См. ", "provider.custom.description.link": "документацию по настройке провайдера", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "ID провайдера", @@ -1324,6 +1332,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Выберите модель транскрипции Kilo Gateway для голосового ввода.", "settings.experimental.continueOnDeny.title": "Продолжить при отказе", "settings.experimental.continueOnDeny.description": "Продолжить цикл агента при отказе в разрешении", + "settings.sandboxing.title": "Изоляция в песочнице", + "settings.sandboxing.network.title": "Ограничить доступ к сети", + "settings.sandboxing.network.description": + "Блокировать исходящий доступ к сети для команд, инициированных моделью, и HTTP-инструментов. Локальные серверы MCP и хуки плагинов не подпадают под это ограничение. Трафик к провайдерам и моделям для инференса остаётся доступным.", "settings.experimental.mcpTimeout.title": "Таймаут MCP (мс)", "settings.experimental.mcpTimeout.description": "Таймаут запросов MCP-сервера в миллисекундах", "settings.experimental.remote.title": "Управление Remote", @@ -1456,6 +1468,9 @@ export const dict = { "Пользовательские команды не настроены. Добавьте команды в opencode.json, чтобы увидеть их здесь.", "settings.agentBehaviour.workflows.detail.description": "Описание", "settings.agentBehaviour.workflows.detail.template": "Шаблон", + "settings.experimental.sandbox.title": "Песочница", + "settings.experimental.sandbox.description": + "Выполнять команды оболочки агента в песочнице на уровне ОС, которая ограничивает запись в каталоги состояния проекта и Kilo", "settings.autoApprove.description": "Определите правила запуска инструментов. Большинство инструментов по умолчанию Разрешены. Для doom_loop и external_directory по умолчанию установлено Спрашивать.", @@ -1556,6 +1571,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Миграция с устаревшей версии", "settings.aboutKiloCode.legacyMigration.description": "Перенос настроек из предыдущей установки Kilo Code, включая API-ключи провайдеров и модель по умолчанию.", + "settings.aboutKiloCode.rooImport.description": "Импорт истории переписки из установки Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Импортировать сеансы из Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Что нового в Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 411f1d672da..6b917cc0428 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -285,6 +285,12 @@ export const dict = { "prompt.action.enhanceDescription": "ปุ่ม 'ปรับปรุงพรอมต์' ช่วยปรับปรุงพรอมต์ของคุณโดยให้บริบทเพิ่มเติม ชี้แจง หรือเขียนใหม่ ลองพิมพ์พรอมต์ที่นี่และคลิกปุ่มอีกครั้งเพื่อดูว่ามันทำงานอย่างไร", "prompt.action.indexing": "การตั้งค่าการสร้างดัชนี", + "prompt.action.sandbox.enable": "เปิดใช้งาน sandbox", + "prompt.action.sandbox.disable": "ปิดใช้งาน sandbox", + "prompt.action.sandbox.enabled": + "เปิดใช้งาน sandbox แล้ว คำสั่ง shell ของ agent ถูกจำกัดไว้ที่โฟลเดอร์โปรเจ็กต์และ Kilo", + "prompt.action.sandbox.disabled": + "ปิดใช้งาน sandbox อยู่ คลิกเพื่อจำกัดการเขียนคำสั่ง shell ของ agent ไว้ที่โฟลเดอร์โปรเจ็กต์และ Kilo", "speechToText.tooltip.start": "เริ่มการป้อนข้อมูลด้วยเสียงด้วย Kilo Gateway", "speechToText.tooltip.stop": "หยุดจับเสียง", @@ -783,6 +789,9 @@ export const dict = { "settings.indexing.title": "การสร้างดัชนี", "settings.indexing.enable.title": "เปิดใช้งานการสร้างดัชนี", "settings.indexing.enable.description": "เปิดหรือปิดการสร้างดัชนีโค้ดเบสเชิงความหมาย", + "settings.indexing.showButton.title": "แสดงปุ่มเมื่อปิดการสร้างดัชนี", + "settings.indexing.showButton.description": + "แสดงปุ่มการสร้างดัชนีใต้ช่องพรอมต์ขณะปิดการสร้างดัชนี หากซ่อนปุ่มไว้ ให้เปิด การตั้งค่า > การสร้างดัชนี เพื่อเปิดใช้การสร้างดัชนี", "settings.indexing.globalEnable.title": "เปิดใช้งานแบบโกลบอล", "settings.indexing.globalEnable.description": "เปิดใช้งานการทำดัชนีสำหรับทุกพื้นที่ทำงาน", "settings.indexing.projectEnable.title": "เปิดใช้งานสำหรับโปรเจกต์นี้", @@ -850,18 +859,18 @@ export const dict = { "settings.providers.tag.customProvider": "ผู้ให้บริการที่กำหนดเอง", "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ", "settings.providers.action.signInChatGPT": "ลงชื่อเข้าใช้ด้วย ChatGPT", - "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่เข้ากันได้กับ OpenAI ด้วย URL พื้นฐาน", + "settings.providers.custom.description": "เพิ่มผู้ให้บริการแบบกำหนดเองด้วย URL พื้นฐาน", "settings.providers.subagentModel.title": "โมเดลตัวแทนย่อย", "settings.providers.subagentModel.description": "โมเดลเริ่มต้นและระดับการใช้เหตุผลสำหรับตัวแทนย่อยของ task-tool ปล่อยว่างไว้เพื่อรับค่าโมเดลจากตัวแทนที่เรียก", "settings.models.hidePromptTraining.title": "ซ่อนโมเดลที่ใช้พรอมต์ในการฝึก", "settings.models.hidePromptTraining.description": "ซ่อนโมเดล Kilo Gateway ที่ผู้ให้บริการอาจใช้พรอมต์ของคุณในการฝึก", "settings.providers.modeModels": "โมเดลต่อโหมด", - "settings.providers.custom.note": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย Base URL", + "settings.providers.custom.note": "เพิ่มผู้ให้บริการแบบกำหนดเองด้วย Base URL", "settings.providers.modeModels.description": "แทนที่โมเดลเริ่มต้นสำหรับโหมดที่กำหนด หากไม่ได้ตั้งค่า จะใช้โมเดลเริ่มต้นทั่วไป", "provider.custom.title": "ผู้ให้บริการที่กำหนดเอง", - "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู", + "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการแบบกำหนดเอง ดู", "provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ", "provider.custom.description.suffix": "", "provider.custom.field.providerID.label": "ID ผู้ให้บริการ", @@ -1303,6 +1312,10 @@ export const dict = { "settings.models.speechToTextModel.description": "เลือกโมเดลการถอดเสียง Kilo Gateway สำหรับการป้อนข้อมูลด้วยเสียง", "settings.experimental.continueOnDeny.title": "ดำเนินต่อเมื่อถูกปฏิเสธ", "settings.experimental.continueOnDeny.description": "ดำเนินลูปเอเจนต์ต่อเมื่อสิทธิ์ถูกปฏิเสธ", + "settings.sandboxing.title": "การทำงานในแซนด์บ็อกซ์", + "settings.sandboxing.network.title": "จำกัดการเข้าถึงเครือข่าย", + "settings.sandboxing.network.description": + "บล็อกการเข้าถึงเครือข่ายขาออกจากคำสั่งที่มาจากโมเดลและเครื่องมือ HTTP เซิร์ฟเวอร์ MCP ภายในเครื่องและฮุกของปลั๊กอินทำงานอยู่นอกข้อจำกัดนี้ การรับส่งข้อมูลสำหรับการอนุมานของผู้ให้บริการและโมเดลยังคงใช้งานได้", "settings.experimental.mcpTimeout.title": "หมดเวลา MCP (มิลลิวินาที)", "settings.experimental.mcpTimeout.description": "หมดเวลาสำหรับคำขอเซิร์ฟเวอร์ MCP เป็นมิลลิวินาที", "settings.experimental.remote.title": "การควบคุม Remote", @@ -1433,6 +1446,9 @@ export const dict = { "ไม่มีคำสั่งแบบกำหนดเองที่กำหนดค่าไว้ เพิ่มคำสั่งใน opencode.json เพื่อดูที่นี่", "settings.agentBehaviour.workflows.detail.description": "คำอธิบาย", "settings.agentBehaviour.workflows.detail.template": "เทมเพลต", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "เรียกใช้คำสั่ง shell ของ agent ใน sandbox ระดับระบบปฏิบัติการที่จำกัดการเขียนไปยังโฟลเดอร์สถานะของโปรเจ็กต์และ Kilo", "settings.autoApprove.description": "กำหนดวิธีอนุญาตการทำงานของเครื่องมือ โดยค่าเริ่มต้นเครื่องมือส่วนใหญ่คืออนุญาต ส่วน doom_loop และ external_directory ค่าเริ่มต้นคือถาม", @@ -1525,6 +1541,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "การย้ายข้อมูลจากเวอร์ชันดั้งเดิม", "settings.aboutKiloCode.legacyMigration.description": "ย้ายการตั้งค่าจากการติดตั้ง Kilo Code ก่อนหน้านี้ รวมถึงคีย์ API ของผู้ให้บริการและโมเดลเริ่มต้น", + "settings.aboutKiloCode.rooImport.description": "นำเข้าประวัติการสนทนาจากการติดตั้ง Roo Code", + "settings.aboutKiloCode.rooImport.button": "นำเข้าเซสชันจาก Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "มีอะไรใหม่ใน Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 041cc4d8749..462b1187da1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -285,6 +285,11 @@ export const dict = { "prompt.action.resetModel": "Modeli varsayılana sıfırla", "prompt.action.enhanceDescription": "'Komutu Geliştir' düğmesi, ek bağlam, açıklama veya yeniden ifadelendirme sağlayarak komutunuzu iyileştirmeye yardımcı olur. Buraya bir komut yazıp düğmeye tekrar tıklayarak nasıl çalıştığını görebilirsiniz.", + "prompt.action.sandbox.enable": "Sandbox'ı etkinleştir", + "prompt.action.sandbox.disable": "Sandbox'ı devre dışı bırak", + "prompt.action.sandbox.enabled": "Sandbox etkin. Agent shell komutları proje ve Kilo dizinleriyle sınırlıdır.", + "prompt.action.sandbox.disabled": + "Sandbox devre dışı. Agent shell komut yazma işlemlerini proje ve Kilo dizinleriyle sınırlamak için tıklayın.", "speechToText.tooltip.start": "Kilo Gateway ile sesli girişi başlatın", "speechToText.tooltip.stop": "Ses yakalamayı durdur", @@ -824,10 +829,10 @@ export const dict = { "settings.providers.tag.other": "Diğer", "settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı", "settings.providers.action.signInChatGPT": "ChatGPT ile oturum aç", - "settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.", + "settings.providers.custom.description": "Temel URL üzerinden özel bir sağlayıcı ekleyin.", "provider.custom.title": "Özel sağlayıcı", - "provider.custom.description.prefix": "OpenAI uyumlu bir sağlayıcı yapılandırın. ", + "provider.custom.description.prefix": "Özel bir sağlayıcı yapılandırın. ", "provider.custom.description.link": "Sağlayıcı yapılandırma dökümanları", "provider.custom.description.suffix": " sayfasına bakın.", "provider.custom.field.providerID.label": "Sağlayıcı kimlik", @@ -1121,6 +1126,9 @@ export const dict = { "settings.indexing.status.title": "Durum", "settings.indexing.enable.title": "İndekslemeyi etkinleştir", "settings.indexing.enable.description": "Anlamsal kod tabanı indekslemeyi açın veya kapatın.", + "settings.indexing.showButton.title": "İndeksleme kapalıyken düğmeyi göster", + "settings.indexing.showButton.description": + "İndeksleme kapalıyken istem alanının altında indeksleme düğmesini göster. Düğme gizliyse indekslemeyi etkinleştirmek için Ayarlar > İndeksleme'yi açın.", "settings.indexing.globalEnable.title": "Genel olarak etkinleştir", "settings.indexing.globalEnable.description": "Her çalışma alanı için dizine eklemeyi etkinleştir.", "settings.indexing.projectEnable.title": "Bu proje için etkinleştir", @@ -1313,6 +1321,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Sesli giriş için Kilo Gateway transkripsiyon modelini seçin.", "settings.experimental.continueOnDeny.title": "Reddetme Durumunda Devam Et", "settings.experimental.continueOnDeny.description": "Bir izin reddedildiğinde ajan döngüsüne devam et", + "settings.sandboxing.title": "Sandbox", + "settings.sandboxing.network.title": "Ağ Erişimini Kısıtla", + "settings.sandboxing.network.description": + "Model tarafından başlatılan komutların ve HTTP araçlarının giden ağ erişimini engelleyin. Yerel MCP sunucuları ve eklenti kancaları bu kısıtlamanın dışında çalışır. Sağlayıcı ve model çıkarım trafiği kullanılabilir durumda kalır.", "settings.experimental.mcpTimeout.title": "MCP Zaman Aşımı (ms)", "settings.experimental.mcpTimeout.description": "MCP sunucu istekleri için milisaniye cinsinden zaman aşımı", "settings.experimental.remote.title": "Remote Kontrolü", @@ -1324,6 +1336,9 @@ export const dict = { "settings.experimental.remote.inactive": "Pasif", "settings.experimental.remote.hint": "Geçiş yapmak için sohbette /remote kullanın", "settings.experimental.toolToggles": "Araç Açma/Kapatma", + "settings.experimental.sandbox.title": "Sandbox", + "settings.experimental.sandbox.description": + "Agent shell komutlarını, proje ve Kilo durum dizinlerine yazmaları kısıtlanan işletim sistemi düzeyinde bir sandbox içinde çalıştırın", "settings.agentBehaviour.defaultAgent.title": "Varsayılan Ajan", "settings.agentBehaviour.defaultAgent.description": "Belirtilmediğinde kullanılacak ajan", @@ -1510,7 +1525,7 @@ export const dict = { "settings.providers.enabled.description": "Ayarlanırsa yalnızca bu sağlayıcılar kullanılabilir olacak (özel izin listesi)", "settings.providers.notSet": "Ayarlanmadı (sunucu varsayılanını kullan)", - "settings.providers.custom.note": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.", + "settings.providers.custom.note": "Temel URL üzerinden özel bir sağlayıcı ekleyin.", "settings.providers.search.placeholder": "Sağlayıcı ara", "settings.providers.select.placeholder": "Sağlayıcı seç...", @@ -1528,6 +1543,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Eski Sürüm Taşıma", "settings.aboutKiloCode.legacyMigration.description": "Sağlayıcı API anahtarları ve varsayılan model dahil olmak üzere Kilo Code'un önceki yüklemesinden ayarları taşıyın.", + "settings.aboutKiloCode.rooImport.description": "Bir Roo Code yüklemesinden konuşma geçmişini içe aktarın.", + "settings.aboutKiloCode.rooImport.button": "Roo Code'dan Oturumları İçe Aktar", // Screen 1 — What's New "migration.whatsNew.title": "Kilo Code'daki Yenilikler", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 2055e0e5af1..3fd1915ba1a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -287,6 +287,11 @@ export const dict = { "prompt.action.resetModel": "Скинути модель до стандартної", "prompt.action.enhanceDescription": "Кнопка 'Покращити запит' допомагає вдосконалити ваш запит, надаючи додатковий контекст, уточнення або перефразування. Введіть запит тут і натисніть кнопку ще раз, щоб побачити, як це працює.", + "prompt.action.sandbox.enable": "Увімкнути пісочницю", + "prompt.action.sandbox.disable": "Вимкнути пісочницю", + "prompt.action.sandbox.enabled": "Пісочницю увімкнено. Команди оболонки агента обмежені каталогами проєкту та Kilo.", + "prompt.action.sandbox.disabled": + "Пісочницю вимкнено. Натисніть, щоб обмежити запис команд оболонки агента каталогами проєкту та Kilo.", "speechToText.tooltip.start": "Почати голосове введення з Kilo Gateway", "speechToText.tooltip.stop": "Зупинити захоплення звуку", @@ -825,10 +830,10 @@ export const dict = { "settings.providers.tag.other": "Інші", "settings.providers.connected.environmentDescription": "Підключено зі змінних середовища", "settings.providers.action.signInChatGPT": "Увійти через ChatGPT", - "settings.providers.custom.description": "Додати OpenAI-сумісного провайдера через базовий URL.", + "settings.providers.custom.description": "Додати власного провайдера через базовий URL.", "provider.custom.title": "Власний провайдер", - "provider.custom.description.prefix": "Налаштувати OpenAI-сумісного провайдера. ", + "provider.custom.description.prefix": "Налаштувати власного провайдера. ", "provider.custom.description.link": "Документація щодо налаштування провайдера", "provider.custom.description.suffix": ".", "provider.custom.field.providerID.label": "Ідентифікатор провайдера", @@ -1120,6 +1125,9 @@ export const dict = { "settings.indexing.status.title": "Статус", "settings.indexing.enable.title": "Увімкнути індексування", "settings.indexing.enable.description": "Увімкніть або вимкніть семантичне індексування кодової бази.", + "settings.indexing.showButton.title": "Показувати кнопку, коли індексування вимкнено", + "settings.indexing.showButton.description": + "Показувати кнопку індексування під полем введення, поки індексування вимкнено. Якщо кнопку приховано, відкрийте «Налаштування > Індексування», щоб увімкнути індексування.", "settings.indexing.globalEnable.title": "Увімкнути глобально", "settings.indexing.globalEnable.description": "Увімкнути індексування для кожного робочого простору.", "settings.indexing.projectEnable.title": "Увімкнути для цього проєкту", @@ -1312,6 +1320,10 @@ export const dict = { "settings.models.speechToTextModel.description": "Виберіть модель транскрипції Kilo Gateway для голосового введення.", "settings.experimental.continueOnDeny.title": "Продовжувати при відхиленні", "settings.experimental.continueOnDeny.description": "Продовжувати цикл агента, коли дозвіл відхилено", + "settings.sandboxing.title": "Пісочниця", + "settings.sandboxing.network.title": "Обмежити доступ до мережі", + "settings.sandboxing.network.description": + "Блокуйте вихідний доступ до мережі для команд, ініційованих моделлю, та HTTP-інструментів. Локальні MCP-сервери й хуки плагінів працюють поза цим обмеженням. Трафік провайдерів та інференсу моделей залишається доступним.", "settings.experimental.mcpTimeout.title": "Тайм-аут MCP (мс)", "settings.experimental.mcpTimeout.description": "Тайм-аут у мілісекундах для запитів до MCP-сервера", "settings.experimental.remote.title": "Керування Remote", @@ -1323,6 +1335,9 @@ export const dict = { "settings.experimental.remote.inactive": "Неактивний", "settings.experimental.remote.hint": "Використовуйте /remote у чаті для перемикання", "settings.experimental.toolToggles": "Перемикачі інструментів", + "settings.experimental.sandbox.title": "Пісочниця", + "settings.experimental.sandbox.description": + "Виконувати команди оболонки агента в пісочниці на рівні ОС, яка обмежує запис до каталогів стану проєкту та Kilo", "settings.agentBehaviour.defaultAgent.title": "Агент за замовчуванням", "settings.agentBehaviour.defaultAgent.description": "Агент, що використовується, якщо не вказано інший", @@ -1507,7 +1522,7 @@ export const dict = { "settings.providers.enabled": "Увімкнені провайдери (список дозволених)", "settings.providers.enabled.description": "Якщо встановлено, доступні лише ці провайдери (власний список дозволених)", "settings.providers.notSet": "Не встановлено (використовувати стандартний сервера)", - "settings.providers.custom.note": "Додати OpenAI-сумісного провайдера через базовий URL.", + "settings.providers.custom.note": "Додати власного провайдера через базовий URL.", "settings.providers.search.placeholder": "Пошук провайдера", "settings.providers.select.placeholder": "Вибрати провайдера...", @@ -1525,6 +1540,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "Перенесення зі старої версії", "settings.aboutKiloCode.legacyMigration.description": "Перенести налаштування з попередньої версії Kilo Code, включаючи API-ключі провайдерів та стандартну модель.", + "settings.aboutKiloCode.rooImport.description": "Імпортувати історію розмов з інсталяції Roo Code.", + "settings.aboutKiloCode.rooImport.button": "Імпортувати сесії з Roo Code", // Screen 1 — What's New "migration.whatsNew.title": "Що нового в Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 1eda7ef35fb..c1414a9f14f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -284,6 +284,10 @@ export const dict = { "prompt.action.resetModel": "重置模型为默认值", "prompt.action.enhanceDescription": "'增强提示'按钮通过提供额外上下文、澄清或重新表述来帮助改进您的请求。尝试在此处输入请求,然后再次点击按钮查看其工作原理。", + "prompt.action.sandbox.enable": "启用沙盒", + "prompt.action.sandbox.disable": "禁用沙盒", + "prompt.action.sandbox.enabled": "沙盒已启用。代理 shell 命令被限制在项目和 Kilo 目录内。", + "prompt.action.sandbox.disabled": "沙盒已禁用。点击以将代理 shell 命令的写入限制在项目和 Kilo 目录内。", "speechToText.tooltip.start": "使用 Kilo Gateway 开始语音输入", "speechToText.tooltip.stop": "停止捕获音频", @@ -772,6 +776,9 @@ export const dict = { "settings.indexing.title": "索引", "settings.indexing.enable.title": "启用索引", "settings.indexing.enable.description": "打开或关闭语义代码库索引。", + "settings.indexing.showButton.title": "索引关闭时显示按钮", + "settings.indexing.showButton.description": + "索引关闭时,在提示词下方显示索引按钮。如果该按钮已隐藏,请打开“设置 > 索引”以启用索引。", "settings.indexing.globalEnable.title": "全局启用", "settings.indexing.globalEnable.description": "为每个工作区启用索引。", "settings.indexing.projectEnable.title": "为此项目启用", @@ -838,16 +845,16 @@ export const dict = { "settings.providers.tag.customProvider": "自定义提供商", "settings.providers.connected.environmentDescription": "从您的环境变量连接", "settings.providers.action.signInChatGPT": "使用 ChatGPT 登录", - "settings.providers.custom.description": "通过基础 URL 添加 OpenAI 兼容的提供商。", + "settings.providers.custom.description": "通过基础 URL 添加自定义提供商。", "settings.providers.subagentModel.title": "子代理模型", "settings.providers.subagentModel.description": "task-tool 子代理的默认模型和推理工作量。留空以继承调用代理的模型。", "settings.models.hidePromptTraining.title": "隐藏使用提示词训练的模型", "settings.models.hidePromptTraining.description": "隐藏提供商可能会使用您的提示词进行训练的 Kilo Gateway 模型。", "settings.providers.modeModels": "按模式选择模型", - "settings.providers.custom.note": "通过 Base URL 添加 OpenAI 兼容提供商。", + "settings.providers.custom.note": "通过 Base URL 添加自定义提供商。", "settings.providers.modeModels.description": "为特定模式覆盖默认模型。如果未设置,将使用全局默认模型。", "provider.custom.title": "自定义提供商", - "provider.custom.description.prefix": "配置 OpenAI 兼容的提供商。请参阅", + "provider.custom.description.prefix": "配置自定义提供商。请参阅", "provider.custom.description.link": "提供商配置文档", "provider.custom.description.suffix": "。", "provider.custom.field.providerID.label": "提供商 ID", @@ -1283,6 +1290,10 @@ export const dict = { "settings.models.speechToTextModel.description": "选择用于语音输入的 Kilo Gateway 转录模型。", "settings.experimental.continueOnDeny.title": "拒绝后继续", "settings.experimental.continueOnDeny.description": "权限被拒绝时继续智能体循环", + "settings.sandboxing.title": "沙盒", + "settings.sandboxing.network.title": "限制网络访问", + "settings.sandboxing.network.description": + "阻止模型发起的命令和 HTTP 工具进行出站网络访问。本地 MCP 服务器和插件钩子不受此限制。提供商和模型推理流量仍然可用。", "settings.experimental.mcpTimeout.title": "MCP 超时(毫秒)", "settings.experimental.mcpTimeout.description": "MCP 服务器请求的超时时间(毫秒)", "settings.experimental.remote.title": "Remote 控制", @@ -1403,6 +1414,9 @@ export const dict = { "settings.agentBehaviour.workflows.empty": "未配置自定义命令。将命令添加到 opencode.json 即可在此处看到。", "settings.agentBehaviour.workflows.detail.description": "描述", "settings.agentBehaviour.workflows.detail.template": "模板", + "settings.experimental.sandbox.title": "沙盒", + "settings.experimental.sandbox.description": + "在操作系统级沙盒中运行代理 shell 命令,将写入限制在项目和 Kilo 状态目录内", "settings.autoApprove.description": "定义工具的运行权限。大多数工具默认为「允许」。doom_loop 和 external_directory 默认为「询问」。", @@ -1489,6 +1503,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "旧版迁移", "settings.aboutKiloCode.legacyMigration.description": "从旧版 Kilo Code 安装迁移设置,包括提供商 API 密钥和默认模型。", + "settings.aboutKiloCode.rooImport.description": "从 Roo Code 安装导入对话历史记录。", + "settings.aboutKiloCode.rooImport.button": "从 Roo Code 导入会话", // Screen 1 — What's New "migration.whatsNew.title": "Kilo Code 新功能", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index ca439c70171..597f38049aa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -283,6 +283,10 @@ export const dict = { "prompt.action.resetModel": "重置模型為預設值", "prompt.action.enhanceDescription": "「強化提示詞」按鈕可透過提供額外內容、說明或改寫來協助改善提示詞。試著在這裡輸入提示詞,再點選一次按鈕以了解其運作方式。", + "prompt.action.sandbox.enable": "啟用沙盒", + "prompt.action.sandbox.disable": "停用沙盒", + "prompt.action.sandbox.enabled": "沙盒已啟用。代理 shell 指令被限制在專案和 Kilo 目錄內。", + "prompt.action.sandbox.disabled": "沙盒已停用。點擊以將代理 shell 指令的寫入限制在專案和 Kilo 目錄內。", "speechToText.tooltip.start": "使用 Kilo Gateway 開始語音輸入", "speechToText.tooltip.stop": "停止擷取音訊", @@ -806,16 +810,16 @@ export const dict = { "settings.providers.tag.customProvider": "自訂提供商", "settings.providers.connected.environmentDescription": "從您的環境變數連線", "settings.providers.action.signInChatGPT": "使用 ChatGPT 登入", - "settings.providers.custom.description": "透過基礎 URL 新增 OpenAI 相容的提供商。", + "settings.providers.custom.description": "透過基礎 URL 新增自訂提供商。", "settings.providers.subagentModel.title": "子代理模型", "settings.providers.subagentModel.description": "task-tool 子代理的預設模型和推理工作量。留空以繼承呼叫代理的模型。", "settings.models.hidePromptTraining.title": "隱藏使用提示詞訓練的模型", "settings.models.hidePromptTraining.description": "隱藏供應商可能會使用您的提示詞進行訓練的 Kilo Gateway 模型。", "settings.providers.modeModels": "按模式選擇模型", - "settings.providers.custom.note": "透過 Base URL 新增 OpenAI 相容供應商。", + "settings.providers.custom.note": "透過 Base URL 新增自訂供應商。", "settings.providers.modeModels.description": "為特定模式覆寫預設模型。如果未設定,將使用全域預設模型。", "provider.custom.title": "自訂提供商", - "provider.custom.description.prefix": "設定 OpenAI 相容的提供商。請參閱", + "provider.custom.description.prefix": "設定自訂提供商。請參閱", "provider.custom.description.link": "提供商設定文件", "provider.custom.description.suffix": "。", "provider.custom.field.providerID.label": "提供商 ID", @@ -1251,6 +1255,10 @@ export const dict = { "settings.models.speechToTextModel.description": "選擇用於語音輸入的 Kilo Gateway 轉錄模型。", "settings.experimental.continueOnDeny.title": "拒絕後繼續", "settings.experimental.continueOnDeny.description": "權限被拒絕時繼續 Agent 迴圈", + "settings.sandboxing.title": "沙盒", + "settings.sandboxing.network.title": "限制網路存取", + "settings.sandboxing.network.description": + "封鎖模型發起的命令和 HTTP 工具的對外網路存取。本機 MCP 伺服器和外掛程式鉤子不受此限制。供應商與模型推論流量仍然可用。", "settings.experimental.mcpTimeout.title": "MCP 逾時(毫秒)", "settings.experimental.mcpTimeout.description": "MCP 伺服器請求的逾時時間(毫秒)", "settings.experimental.remote.title": "Remote 控制", @@ -1373,6 +1381,9 @@ export const dict = { "settings.agentBehaviour.workflows.empty": "未設定自訂命令。將命令新增至 opencode.json 即可在此處看到。", "settings.agentBehaviour.workflows.detail.description": "描述", "settings.agentBehaviour.workflows.detail.template": "範本", + "settings.experimental.sandbox.title": "沙盒", + "settings.experimental.sandbox.description": + "在作業系統層級沙盒中執行代理 shell 指令,將寫入限制在專案和 Kilo 狀態目錄內", "settings.autoApprove.description": "定義工具的執行權限。大多數工具預設為允許。doom_loop 與 external_directory 預設為詢問。", @@ -1456,6 +1467,9 @@ export const dict = { "settings.indexing.dimension.placeholder": "自動", "settings.indexing.dimension.title": "向量維度", "settings.indexing.enable.description": "開啟或關閉語意程式碼庫索引。", + "settings.indexing.showButton.title": "索引關閉時顯示按鈕", + "settings.indexing.showButton.description": + "索引關閉時,在提示詞下方顯示索引按鈕。如果該按鈕已隱藏,請開啟「設定 > 索引」以啟用索引。", "settings.indexing.enable.title": "啟用索引", "settings.indexing.globalEnable.title": "全域啟用", "settings.indexing.globalEnable.description": "為每個工作區啟用索引。", @@ -1495,6 +1509,8 @@ export const dict = { "settings.aboutKiloCode.legacyMigration.title": "舊版遷移", "settings.aboutKiloCode.legacyMigration.description": "從舊版 Kilo Code 安裝遷移設定,包括供應商 API 金鑰和預設模型。", + "settings.aboutKiloCode.rooImport.description": "從 Roo Code 安裝匯入對話歷史記錄。", + "settings.aboutKiloCode.rooImport.button": "從 Roo Code 匯入工作階段", // Screen 1 — What's New "migration.whatsNew.title": "Kilo Code 新功能", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index 749b1a6c679..4e9c67b0057 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -41,6 +41,7 @@ import { hasIndexingPlugin } from "@kilocode/kilo-indexing/detect" import { resolveTemplate } from "../context/language-utils" import type { Config, + FeatureFlags, KilocodeNotification, PermissionRequest, ProviderAuthState, @@ -117,7 +118,7 @@ export const defaultMockData = { part: {} as Record, permission: {} as Record, question: {}, - provider: { all: [], connected: false, default: {} }, + provider: { all: new Map(), connected: [], default: {} }, } // --------------------------------------------------------------------------- @@ -280,6 +281,7 @@ interface StoryProvidersProps { sessionID?: string /** When provided, injects a mock ConfigContext with this config instead of the real ConfigProvider. */ config?: Config + features?: Partial globalConfig?: Config projectConfig?: Config onConfigChange?: (config: Config) => void @@ -295,6 +297,7 @@ interface StoryProvidersProps { /** Wraps children with either a mock ConfigContext (when config prop is given) or the real ConfigProvider. */ const ConfigWrapper: ParentComponent<{ config?: Config + features?: Partial globalConfig?: Config projectConfig?: Config onConfigChange?: (config: Config) => void @@ -314,7 +317,8 @@ const ConfigWrapper: ParentComponent<{ } return { - indexing: hasIndexingPlugin(config.plugin ?? []), + indexing: props.features?.indexing ?? hasIndexingPlugin(config.plugin ?? []), + sandboxControls: props.features?.sandboxControls ?? false, } }) @@ -388,6 +392,7 @@ export const StoryProviders: ParentComponent = (props) => { = (props session_diff: {}, message: {}, part: {}, - provider: { all: [], connected: [] as string[], default: {} as any, failed: [] as string[] }, + provider: { + all: new Map(), + connected: [] as string[], + default: {} as any, + }, }} directory="/project/" > diff --git a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx index 62b7f321f1b..b8c0d692b67 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx @@ -115,7 +115,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } }', parameters: [{ name: "GitHub Token", key: "GITHUB_TOKEN", placeholder: "ghp_xxxxxxxxxxxx" }], author: "Anthropic", - tags: ["version-control", "development"], + category: "development", }, { type: "mcp", @@ -143,7 +143,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ }, ], author: "Anthropic", - tags: ["database", "sql"], + category: "data", }, { type: "mcp", @@ -153,7 +153,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ url: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem", content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "${ALLOWED_DIR}"] }', parameters: [{ name: "Allowed Directory", key: "ALLOWED_DIR", placeholder: "/path/to/directory" }], - tags: ["filesystem", "development"], + category: "development", }, { type: "mcp", @@ -165,7 +165,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-slack"], "env": { "SLACK_TOKEN": "${SLACK_TOKEN}" } }', parameters: [{ name: "Slack Bot Token", key: "SLACK_TOKEN", placeholder: "xoxb-xxxxxxxxxxxx" }], author: "Anthropic", - tags: ["communication", "productivity"], + category: "productivity", }, { type: "mcp", @@ -176,7 +176,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "${BRAVE_API_KEY}" } }', parameters: [{ name: "API Key", key: "BRAVE_API_KEY", placeholder: "BSA-xxxxxxxxxxxx" }], - tags: ["search", "web"], + category: "search", }, { type: "mcp", @@ -186,7 +186,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [ url: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer", content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-puppeteer"] }', prerequisites: ["Chrome or Chromium must be installed"], - tags: ["browser", "automation", "web"], + category: "web-automation", }, ] @@ -205,7 +205,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [ permission: { read: "allow", edit: "deny", bash: "deny", mcp: "deny", question: "allow" }, }, author: "Kilo", - tags: ["planning", "design"], + category: "development", }, { type: "agent", @@ -221,7 +221,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [ permission: { read: "allow", edit: "deny", bash: "allow", mcp: "deny", question: "allow" }, }, author: "Kilo", - tags: ["review", "quality"], + category: "development", }, { type: "agent", @@ -235,7 +235,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [ options: { displayName: "Documentation Writer" }, permission: { read: "allow", edit: "allow", bash: "allow", mcp: "deny", question: "allow" }, }, - tags: ["documentation", "writing"], + category: "business", }, { type: "agent", @@ -251,7 +251,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [ permission: { read: "allow", edit: "allow", bash: "allow", mcp: "allow", question: "allow" }, }, author: "Community", - tags: ["testing", "methodology"], + category: "development", }, { type: "agent", @@ -265,25 +265,33 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [ options: { displayName: "Debugger" }, permission: { read: "allow", edit: "allow", bash: "allow", mcp: "deny", question: "allow" }, }, - tags: ["debugging", "troubleshooting"], + category: "development", }, ] const EMPTY_METADATA: MarketplaceInstalledMetadata = { project: {}, global: {} } const PARTIAL_INSTALLED_SKILLS: MarketplaceInstalledMetadata = { - project: { "nextjs-developer": { type: "skill" } }, - global: { "python-data-science": { type: "skill" } }, + project: { "skill:nextjs-developer": { type: "skill" } }, + global: { "skill:python-data-science": { type: "skill" } }, } const PARTIAL_INSTALLED_MCPS: MarketplaceInstalledMetadata = { - project: { "github-mcp": { type: "mcp" } }, - global: { "postgres-mcp": { type: "mcp" } }, + project: { "mcp:github-mcp": { type: "mcp" } }, + global: { "mcp:postgres-mcp": { type: "mcp" } }, } const PARTIAL_INSTALLED_AGENTS: MarketplaceInstalledMetadata = { - project: { architect: { type: "agent" } }, - global: { reviewer: { type: "agent" } }, + project: { "agent:architect": { type: "agent" } }, + global: { "agent:reviewer": { type: "agent" } }, +} + +const PARTIAL_INSTALLED_MIXED: MarketplaceInstalledMetadata = { + project: { + "agent:architect": { type: "agent" }, + "mcp:github-mcp": { type: "mcp" }, + }, + global: { "skill:python-data-science": { type: "skill" } }, } const noop = () => {} @@ -292,18 +300,17 @@ const noop = () => {} // Stories // --------------------------------------------------------------------------- -export const SkillsTabWithItems: Story = { - name: "Skills tab — with items", +export const MixedListWithItems: Story = { + name: "Mixed list — categories and installed items", render: () => ( -
+
@@ -312,38 +319,17 @@ export const SkillsTabWithItems: Story = { ), } -export const SkillsTabWithInstalled: Story = { - name: "Skills tab — some installed", +export const EmptyList: Story = { + name: "Mixed list — empty state", render: () => ( -
- -
-
- ), -} - -export const SkillsTabEmpty: Story = { - name: "Skills tab — empty state", - render: () => ( - -
+
@@ -392,66 +378,6 @@ export const InstalledSkillCard: Story = { // MCP Stories // --------------------------------------------------------------------------- -export const McpTabWithItems: Story = { - name: "MCP tab — with items", - render: () => ( - -
- -
-
- ), -} - -export const McpTabWithInstalled: Story = { - name: "MCP tab — some installed", - render: () => ( - -
- -
-
- ), -} - -export const McpTabEmpty: Story = { - name: "MCP tab — empty state", - render: () => ( - -
- -
-
- ), -} - export const SingleMcpCard: Story = { name: "ItemCard — single MCP not installed", render: () => ( @@ -490,66 +416,6 @@ export const InstalledMcpCard: Story = { // Mode Stories // --------------------------------------------------------------------------- -export const AgentsTabWithItems: Story = { - name: "Agents tab — with items", - render: () => ( - -
- -
-
- ), -} - -export const AgentsTabWithInstalled: Story = { - name: "Agents tab — some installed", - render: () => ( - -
- -
-
- ), -} - -export const AgentsTabEmpty: Story = { - name: "Agents tab — empty state", - render: () => ( - -
- -
-
- ), -} - export const SingleAgentCard: Story = { name: "ItemCard — single agent not installed", render: () => ( diff --git a/packages/kilo-vscode/webview-ui/src/stories/migration.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/migration.stories.tsx new file mode 100644 index 00000000000..4b111e795be --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/stories/migration.stories.tsx @@ -0,0 +1,62 @@ +/** @jsxImportSource solid-js */ +/** + * Stories for migration flows. + */ + +import type { Meta, StoryObj } from "storybook-solidjs-vite" +import { onMount, type Component } from "solid-js" +import MigrationWizard from "../components/migration/MigrationWizard" +import { StoryProviders } from "./StoryProviders" + +const meta: Meta = { + title: "Migration", + parameters: { layout: "fullscreen" }, +} +export default meta +type Story = StoryObj + +const operationId = "roo-migration-story" + +const RooWizard: Component = () => { + onMount(() => { + queueMicrotask(() => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: "migrationData", + source: "roo", + operationId, + data: { + providers: [], + mcpServers: [], + customModes: [], + sessions: [ + { + id: "roo-1", + title: "Refactor authentication flow", + directory: "/workspace/app", + time: 1760443200000, + }, + { id: "roo-2", title: "Add billing dashboard", directory: "/workspace/app", time: 1760356800000 }, + { id: "roo-3", title: "Investigate flaky tests", directory: "/workspace/app", time: 1760270400000 }, + ], + }, + }, + }), + ) + }) + }) + + return {}} onComplete={() => {}} /> +} + +export const RooWizardSelecting: Story = { + name: "Roo wizard — selecting sessions", + render: () => ( + +
+ +
+
+ ), +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index 755554ec02c..3743b388285 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -51,6 +51,20 @@ export const SettingsPanel: Story = { ), } +export const SandboxingPanel: Story = { + name: "Settings — sandboxing network restriction", + render: () => ( + +
+ +
+
+ ), +} + export const ProvidersConfigure: Story = { name: "ProvidersTab — no providers configured", render: () => ( diff --git a/packages/kilo-vscode/webview-ui/src/stories/shell.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shell.stories.tsx index 08310d22c58..2b86a4a21aa 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/shell.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/shell.stories.tsx @@ -51,7 +51,7 @@ const mockData = { part: { [MSG_ID]: [shellPart] }, permission: {}, question: {}, - provider: { all: [], connected: false, default: {} }, + provider: { all: new Map(), connected: [], default: {} }, } const meta: Meta = { diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index fbea166affd..dcb17a5a6a2 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -18,7 +18,7 @@ min-height: 0; overflow: hidden; container: chat / inline-size; - --chat-readable-width: 88ch; + --chat-readable-width: 98ch; --chat-gutter: clamp(16px, 4cqi, 48px); --chat-scrollbar-width: 10px; } diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index 7140c77dd83..387d1bd395e 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -437,6 +437,11 @@ opacity: 0.8; } +.model-selector-data-badge--byok [data-component="tag"] { + background: var(--vscode-badge-background) !important; + color: var(--vscode-badge-foreground) !important; +} + .model-selector-trigger-free-data { display: inline-flex; align-items: center; diff --git a/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css b/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css index 86a473842fb..0eb470ffc2c 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css +++ b/packages/kilo-vscode/webview-ui/src/styles/prompt-input.css @@ -560,11 +560,11 @@ color: var(--vscode-errorForeground, #f48771); } -.prompt-auto-approve-button { +.prompt-status-button { position: relative; } -.prompt-auto-approve-button--active { +.prompt-status-button--active { color: var(--vscode-testing-iconPassed, #73c991); [data-slot="icon-svg"] { @@ -572,7 +572,7 @@ } } -.prompt-auto-approve-button--active::after { +.prompt-status-button--active::after { content: ""; position: absolute; right: 4px; diff --git a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts index 27f83a2b782..954c4b6c159 100644 --- a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts +++ b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts @@ -16,9 +16,9 @@ export interface MarketplaceItemBase { id: string name: string description: string + category: string author?: string authorUrl?: string - tags?: string[] prerequisites?: string[] } @@ -44,7 +44,6 @@ export interface AgentMarketplaceItem extends MarketplaceItemBase { export interface SkillMarketplaceItem extends MarketplaceItemBase { type: "skill" - category: string githubUrl: string content: string displayName: string @@ -66,5 +65,5 @@ export interface MarketplaceInstalledMetadata { export interface MarketplaceFilters { type?: string search?: string - tags?: string[] + categories?: string[] } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index aa4f4271841..65ce65617ef 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -44,6 +44,8 @@ export interface ExperimentalConfig { primary_tools?: string[] continue_loop_on_deny?: boolean mcp_timeout?: number + sandbox?: boolean + sandbox_restrict_network?: boolean } export interface CommitMessageConfig { @@ -147,4 +149,5 @@ export interface Config { export interface FeatureFlags { indexing: boolean + sandboxControls: boolean } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 7debeff4782..330da8f633b 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -39,10 +39,10 @@ import type { WorktreeState, } from "./agent-manager" import type { - LegacyMigrationCompleteMessage, - LegacyMigrationDataMessage, - LegacyMigrationProgressMessage, - LegacyMigrationSessionProgressMessage, + MigrationCompleteMessage, + MigrationDataMessage, + MigrationProgressMessage, + MigrationSessionProgressMessage, MigrationStateMessage, } from "./migration" @@ -314,6 +314,13 @@ export interface IndexingStatusLoadedMessage { status: IndexingStatus } +export interface IndexingSettingsLoadedMessage { + type: "indexingSettingsLoaded" + settings: { + showButtonWhenDisabled: boolean + } +} + export interface KiloEmbeddingModelsLoadedMessage { type: "kiloEmbeddingModelsLoaded" catalog: KiloEmbeddingModelCatalog @@ -637,6 +644,27 @@ export interface AutoApproveStateMessage { active: boolean } +export interface SandboxStatusMessage { + type: "sandboxStatus" + sessionID: string + enabled: boolean + available: boolean + reason?: string + version: number + directory: string + revision: number + requestID?: string +} + +export interface SandboxStatusErrorMessage { + type: "sandboxStatusError" + sessionID: string + directory: string + message: string + revision: number + requestID?: string +} + // Multi-version creation progress (extension → webview) export interface AgentManagerMultiVersionProgressMessage { type: "agentManager.multiVersionProgress" @@ -994,6 +1022,7 @@ export type ExtensionMessage = | DeviceAuthCancelledMessage | NavigateMessage | IndexingStatusLoadedMessage + | IndexingSettingsLoadedMessage | KiloEmbeddingModelsLoadedMessage | ProvidersLoadedMessage | AgentsLoadedMessage @@ -1037,6 +1066,8 @@ export type ExtensionMessage = | AgentManagerRunStatusMessage | AgentManagerKeybindingsMessage | AutoApproveStateMessage + | SandboxStatusMessage + | SandboxStatusErrorMessage | AgentManagerMultiVersionProgressMessage | AgentManagerSetSessionModelMessage | AgentManagerSendInitialMessage @@ -1069,10 +1100,10 @@ export type ExtensionMessage = | AgentManagerTerminalErrorMessage // legacy-migration start | MigrationStateMessage - | LegacyMigrationDataMessage - | LegacyMigrationProgressMessage - | LegacyMigrationSessionProgressMessage - | LegacyMigrationCompleteMessage + | MigrationDataMessage + | MigrationProgressMessage + | MigrationSessionProgressMessage + | MigrationCompleteMessage // legacy-migration end | EnhancePromptResultMessage | EnhancePromptErrorMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/migration.ts b/packages/kilo-vscode/webview-ui/src/types/messages/migration.ts index c9375de144b..c7f7bebc75f 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/migration.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/migration.ts @@ -54,21 +54,18 @@ export interface MigrationResultItem { message?: string } +export type MigrationSource = "legacy" | "roo" + export interface MigrationStateMessage { type: "migrationState" needed: boolean - data?: { - providers: MigrationProviderInfo[] - mcpServers: MigrationMcpServerInfo[] - customModes: MigrationCustomModeInfo[] - sessions?: MigrationSessionInfo[] - defaultModel?: { provider: string; model: string } - settings?: LegacySettings - } + source: "legacy" } -export interface LegacyMigrationDataMessage { - type: "legacyMigrationData" +export interface MigrationDataMessage { + type: "migrationData" + source: MigrationSource + operationId: string data: { providers: MigrationProviderInfo[] mcpServers: MigrationMcpServerInfo[] @@ -79,8 +76,10 @@ export interface LegacyMigrationDataMessage { } } -export interface LegacyMigrationProgressMessage { - type: "legacyMigrationProgress" +export interface MigrationProgressMessage { + type: "migrationProgress" + source: MigrationSource + operationId: string item: string status: "migrating" | "success" | "warning" | "error" message?: string @@ -88,8 +87,10 @@ export interface LegacyMigrationProgressMessage { export type LegacyMigrationSessionPhase = "preparing" | "storing" | "skipped" | "done" | "summary" | "error" -export interface LegacyMigrationSessionProgressMessage { - type: "legacyMigrationSessionProgress" +export interface MigrationSessionProgressMessage { + type: "migrationSessionProgress" + source: MigrationSource + operationId: string session: MigrationSessionInfo index: number total: number @@ -97,13 +98,17 @@ export interface LegacyMigrationSessionProgressMessage { error?: string } -export interface LegacyMigrationCompleteMessage { - type: "legacyMigrationComplete" +export interface MigrationCompleteMessage { + type: "migrationComplete" + source: MigrationSource + operationId: string results: MigrationResultItem[] } -export interface RequestLegacyMigrationDataMessage { - type: "requestLegacyMigrationData" +export interface RequestMigrationDataMessage { + type: "requestMigrationData" + source: MigrationSource + operationId: string } export interface MigrationAutoApprovalSelections { @@ -120,8 +125,10 @@ export interface MigrationSessionSelection { force?: boolean } -export interface StartLegacyMigrationMessage { - type: "startLegacyMigration" +export interface StartMigrationMessage { + type: "startMigration" + source: MigrationSource + operationId: string selections: { providers: string[] mcpServers: string[] diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts index f720ab8841a..81367237f11 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/parts.ts @@ -65,6 +65,10 @@ export interface StepStartPart extends BasePart { export interface StepFinishPart extends BasePart { type: "step-finish" reason?: string + model?: { + providerID: string + modelID: string + } cost?: number tokens?: { input: number diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts index 67c865be0b7..9726f263f36 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts @@ -19,6 +19,7 @@ export interface ProviderModel { recommendedIndex?: number isFree?: boolean mayTrainOnYourPrompts?: boolean + hasUserByokAvailable?: boolean terminalBench?: { overallScore: number avgAttemptCostUsd: number diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index b47d1df7fee..781df064c8e 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -10,9 +10,9 @@ import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-styl import type { ClearLegacyDataMessage, FinalizeLegacyMigrationMessage, - RequestLegacyMigrationDataMessage, + RequestMigrationDataMessage, SkipLegacyMigrationMessage, - StartLegacyMigrationMessage, + StartMigrationMessage, } from "./migration" // ============================================ @@ -430,6 +430,10 @@ export interface RequestIndexingStatusMessage { type: "requestIndexingStatus" } +export interface RequestIndexingSettingsMessage { + type: "requestIndexingSettings" +} + export interface RequestKiloEmbeddingModelsMessage { type: "requestKiloEmbeddingModels" } @@ -920,6 +924,20 @@ export interface ToggleAutoApproveMessage { type: "toggleAutoApprove" } +export interface RequestSandboxStatusMessage { + type: "requestSandboxStatus" + sessionID: string +} + +export interface ToggleSandboxMessage { + type: "toggleSandbox" + sessionID?: string + draftID?: string + requestID: string + agentManagerContext?: string + contextDirectory?: string +} + export interface ToggleRemoteMessage { type: "toggleRemote" } @@ -1164,6 +1182,7 @@ export type WebviewMessage = | RequestConfigMessage | RequestGlobalConfigMessage | RequestIndexingStatusMessage + | RequestIndexingSettingsMessage | RequestKiloEmbeddingModelsMessage | UpdateConfigMessage | OpenSettingsTabRequest @@ -1224,8 +1243,8 @@ export type WebviewMessage = | RefreshPRMessage | OpenPRMessage // legacy-migration start - | RequestLegacyMigrationDataMessage - | StartLegacyMigrationMessage + | RequestMigrationDataMessage + | StartMigrationMessage | SkipLegacyMigrationMessage | ClearLegacyDataMessage | FinalizeLegacyMigrationMessage @@ -1252,6 +1271,8 @@ export type WebviewMessage = | AgentManagerOpenSessionsMessage | RequestAutoApproveStateMessage | ToggleAutoApproveMessage + | RequestSandboxStatusMessage + | ToggleSandboxMessage | FetchMarketplaceDataMessage | FilterMarketplaceItemsMessage | InstallMarketplaceItemMessage diff --git a/packages/kilo-vscode/webview-ui/src/utils/prompt-drafts.ts b/packages/kilo-vscode/webview-ui/src/utils/prompt-drafts.ts index 5ca55cd99e3..7b10da48362 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/prompt-drafts.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/prompt-drafts.ts @@ -13,3 +13,26 @@ export function scopeDraftKey(box: string, raw?: string): string { if (!raw) return `${box}:empty` return `${box}:${raw}` } + +export function createdDraftKey(draftID?: string, sandbox = false): string | undefined { + return pendingDraftKey(draftID) ?? (sandbox ? "new" : undefined) +} + +export function movePromptDraft( + stores: { text: Map; comments: Map; images: Map }, + source: string, + target: string, +): { text?: T; comments?: C; images?: I } { + const draft = { + text: stores.text.get(source), + comments: stores.comments.get(source), + images: stores.images.get(source), + } + if (draft.text !== undefined) stores.text.set(target, draft.text) + if (draft.comments !== undefined) stores.comments.set(target, draft.comments) + if (draft.images !== undefined) stores.images.set(target, draft.images) + stores.text.delete(source) + stores.comments.delete(source) + stores.images.delete(source) + return draft +} diff --git a/packages/kilo-vscode/webview-ui/src/utils/timeline/colors.ts b/packages/kilo-vscode/webview-ui/src/utils/timeline/colors.ts index 88d3796fb9a..686461cd50c 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/timeline/colors.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/timeline/colors.ts @@ -6,7 +6,7 @@ * taskTimelineColorPalette from messageColors.ts. */ -import type { Part, ToolPart } from "../../types/messages" +import type { Message, Part, ToolPart } from "../../types/messages" // ── Color palette (VS Code CSS variables) ──────────────────────────── // These mirror the legacy extension's taskTimelineColorPalette exactly. @@ -71,7 +71,11 @@ export function color(part: Part): TimelineColor { // ── Label for tooltip ──────────────────────────────────────────────── -export function label(part: Part): string { +function auto(msg?: Message) { + return msg?.providerID === "kilo" && msg.modelID?.startsWith("kilo-auto/") +} + +export function label(part: Part, msg?: Message): string { switch (part.type) { case "text": return "Text" @@ -84,7 +88,7 @@ export function label(part: Part): string { case "step-start": return "Step start" case "step-finish": - return "Step finish" + return auto(msg) && part.model?.modelID ? `Step finish · ${part.model.modelID}` : "Step finish" default: return "Unknown" } diff --git a/packages/kilo-web-ui/package.json b/packages/kilo-web-ui/package.json index e380f5b16df..364981a5aa8 100644 --- a/packages/kilo-web-ui/package.json +++ b/packages/kilo-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-web-ui", - "version": "7.3.46", + "version": "7.3.54", "type": "module", "license": "MIT", "exports": { diff --git a/packages/llm/AGENTS.md b/packages/llm/AGENTS.md index 16a58fd8662..29cb71f1c47 100644 --- a/packages/llm/AGENTS.md +++ b/packages/llm/AGENTS.md @@ -10,7 +10,7 @@ ## Conventions -Per-type constructors live on the type's namespace, not as top-level re-exports. Use `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for the request-shaped call API: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.model`, `LLM.updateRequest`, `LLM.generateObject`. Two ways to construct the same thing is one too many. +Per-type constructors live on the type, not as top-level re-exports. Use `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many. ## Tests @@ -21,13 +21,22 @@ Per-type constructors live on the type's namespace, not as top-level re-exports. This package is an Effect Schema-first LLM core. The Schema classes in `src/schema/` are the canonical runtime data model. Convenience functions in `src/llm.ts` are thin constructors that return those same Schema class instances; they should improve callsites without creating a second model. +Primary in-repo integration point: + +- `packages/opencode/src/session/llm.ts` is the session-owned orchestration layer that decides whether a request uses AI SDK or this package's native route runtime. +- `packages/opencode/src/session/llm/native-request.ts` is the lowering adapter from opencode's session/AI SDK-shaped data into this package's `LLMRequest` model. +- `packages/opencode/src/session/llm/native-runtime.ts` is the execution adapter that calls `LLMClient.stream(...)` and bridges opencode tools into this package's tool runtime. +- `packages/opencode/src/session/llm/ai-sdk.ts` keeps the default AI SDK path compatible by converting AI SDK stream parts into this package's shared `LLMEvent`s. + +Keep this package independent of session concerns. Session auth, permissions, plugins, telemetry headers, and runtime selection belong in `packages/opencode/src/session/llm.ts` and its local adapters. + ### Request Flow The intended callsite is: ```ts const request = LLM.request({ - model: OpenAI.model("gpt-4o-mini", { apiKey }), + model: OpenAI.configure({ apiKey }).responses("gpt-4o-mini"), system: "You are concise.", prompt: "Say hello.", }) @@ -35,7 +44,7 @@ const request = LLM.request({ const response = yield * LLMClient.generate(request) ``` -`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` selects a registered route by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`. +`LLM.request(...)` builds an `LLMRequest`. `LLMClient.generate(...)` reads the executable route carried by `request.model.route`, builds the provider-native body, asks the route's transport for a real `HttpClientRequest.HttpClientRequest`, sends it through `RequestExecutor.Service`, parses the provider stream into common `LLMEvent`s, and finally returns an `LLMResponse`. Use `LLMClient.stream(request)` when callers want incremental `LLMEvent`s. Use `LLMClient.generate(request)` when callers want those same events collected into an `LLMResponse`. Use `LLMClient.prepare(request)` to compile a request through the route pipeline without sending it — the optional `Body` type argument narrows `.body` to the route's native shape (e.g. `prepare(...)` returns a `PreparedRequestOf`). The runtime body is identical; the generic is a type-level assertion. @@ -46,8 +55,8 @@ Filter or narrow `LLMEvent` streams with `LLMEvent.is.*` (camelCase guards, e.g. A route is the registered, runnable composition of four orthogonal pieces: - **`Protocol`** (`src/route/protocol.ts`) — semantic API contract. Owns request body construction (`body.from`), the body schema (`body.schema`), the streaming-event schema (`stream.event`), and the event-to-`LLMEvent` state machine (`stream.step`). `Route.make(...)` validates and JSON-encodes the body from `body.schema` and decodes frames with `stream.event`. Examples: `OpenAIChat.protocol`, `OpenAIResponses.protocol`, `AnthropicMessages.protocol`, `Gemini.protocol`, `BedrockConverse.protocol`. -- **`Endpoint`** (`src/route/endpoint.ts`) — path construction. The host always lives on `model.baseURL`; the endpoint just supplies the path. `Endpoint.path("/chat/completions")` is the common case; pass a function for paths that embed the model id or a body field (e.g. `Endpoint.path(({ body }) => `/model/${body.modelId}/converse-stream`)`). -- **`Auth`** (`src/route/auth.ts`) — per-request transport authentication. Routes read `model.apiKey` at request time via `Auth.bearer` (the default; sets `Authorization: Bearer `) or `Auth.apiKeyHeader(name)` for providers that use a custom header (Anthropic `x-api-key`, Gemini `x-goog-api-key`). Routes that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result. +- **`Endpoint`** (`src/route/endpoint.ts`) — URL construction. The host, path, and route query live on the endpoint. `Endpoint.path("/chat/completions", { baseURL })` is the common case; pass a function for paths that embed the model id or a body field (e.g. `Endpoint.path(({ body }) => `/model/${body.modelId}/converse-stream`)`). +- **`Auth`** (`src/route/auth.ts`) — per-request transport authentication. Provider facades configure credentials onto the route before model selection, usually via `Auth.bearer(apiKey)` or `Auth.header(name, apiKey)`. Routes that need per-request signing (Bedrock SigV4, future Vertex IAM, Azure AAD) implement `Auth` as a function that signs the body and merges signed headers into the result. - **`Framing`** (`src/route/framing.ts`) — bytes → frames. SSE (`Framing.sse`) is shared; Bedrock keeps its AWS event-stream framing as a typed `Framing` value alongside its protocol. Compose them via `Route.make(...)`: @@ -57,55 +66,52 @@ export const route = Route.make({ id: "openai-chat", provider: "openai", protocol: OpenAIChat.protocol, - transport: HttpTransport.httpJson({ - endpoint: Endpoint.path("/chat/completions"), - auth: Auth.bearer(), - framing: Framing.sse, - encodeBody, - }), - defaults: { + endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.openai.com/v1", - capabilities: capabilities({ tools: { calls: true, streamingInput: true } }), - }, + }), + auth: Auth.bearer(), + framing: Framing.sse, }) ``` +Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `Model` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `LLMError`s. + The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit. -When a provider ships a non-HTTP transport (OpenAI's WebSocket Responses backend, hypothetical bidirectional streaming APIs), the seam is `Transport` — `WebSocketTransport.json(...)` constructs a transport whose `prepare` builds a WebSocket URL and message and whose `frames` yields decoded text from the socket. Same protocol, different transport. +When a provider ships a non-HTTP transport (OpenAI's WebSocket Responses backend, hypothetical bidirectional streaming APIs), the seam is `Transport` — `WebSocketTransport.jsonTransport.with(...)` constructs an IO template whose `prepare` receives the route endpoint/auth at compile time, builds a WebSocket URL and message, and whose `frames` yields decoded text from the socket. Same protocol and endpoint source, different transport. ### URL Construction -`model.baseURL` is required; `Endpoint` only carries the path. Each protocol's `Route.make` includes a canonical URL in `defaults.baseURL` (e.g. `https://api.openai.com/v1`); provider helpers can override by passing `baseURL` in their input. Routes that have no canonical URL (OpenAI-compatible Chat, GitHub Copilot) set `baseURL: string` (required) on their input type so TypeScript catches a missing host at the call site. +`Endpoint` owns `{ baseURL, path, query }`. Each protocol route includes a canonical endpoint when the provider has one (e.g. `https://api.openai.com/v1`); provider helpers override endpoint fields by configuring the route before selecting a model. Routes that have no canonical URL (OpenAI-compatible Chat, GitHub Copilot) require configuration before execution. -For providers where the URL is derived from typed inputs (Azure resource name, Bedrock region), the provider helper computes `baseURL` at model construction time. Use `AtLeastOne` from `route/auth-options.ts` for inputs that accept either of two derivation paths (Azure: `resourceName` or `baseURL`). +For providers where the URL is derived from typed inputs (Azure resource name, Bedrock region), the provider helper configures the route endpoint before calling `.model(...)`. Use `AtLeastOne` from `route/auth-options.ts` for inputs that accept either of two derivation paths (Azure: `resourceName` or `baseURL`). -### Provider Definitions +### Provider Facades -Provider-facing APIs are defined with `Provider.make(...)` from `src/provider.ts`: +Provider-facing APIs are configured facades over route values. Endpoint/auth/resource/API-version setup happens before model selection, and model selectors accept only a model or deployment id: ```ts -export const provider = Provider.make({ - id: ProviderID.make("openai"), - model: responses, - apis: { responses, chat }, -}) +const openai = OpenAI.configure({ apiKey, baseURL }) +const model = openai.responses("gpt-4o-mini") -export const model = provider.model -export const apis = provider.apis +const azure = Azure.configure({ resourceName, apiKey, apiVersion: "v1" }) +const deployment = azure.responses("my-deployment") + +const gateway = CloudflareAIGateway.configure({ accountId, gatewayId, gatewayApiKey, apiKey }) +const proxied = gateway.model("openai/gpt-4o-mini") ``` -Keep provider definitions small and explicit: +Keep provider facades small and explicit: -- Use only `id`, `model`, and optional `apis` in `Provider.make(...)`. - Use branded `ProviderID.make(...)` and `ModelID.make(...)` where ids are constructed directly. -- Use `model` for the default API path and `apis` for named provider-native alternatives such as OpenAI `responses` versus `chat`. -- Do not add author-facing `kind`, `version`, or `routes` fields. +- Use `model` for the default API path and named methods for provider-native alternatives such as OpenAI `responses`, `responsesWebSocket`, and `chat`. +- Put provider-specific setup on `.configure(...)`; do not add `model(id, overrides)` as a duplicate construction path. - Export lower-level `routes` arrays separately only when advanced internal wiring needs them. - Prefer `apiKey` as provider-specific sugar and `auth` as the explicit override; keep them mutually exclusive in provider option types with `ProviderAuthOption`. - Resolve `apiKey` → `Auth` with `AuthOptions.bearer(options, "_API_KEY")` (it honors an explicit `auth` override and falls back to `Auth.config(envVar)` so missing keys surface a typed `Authentication` error rather than a runtime crash). +- Use separate top-level facades for products with different required setup, such as `CloudflareAIGateway` and `CloudflareWorkersAI`. -Built-in providers are namespace modules from `src/providers/index.ts`, so aliases like `OpenAI.model(...)`, `OpenAI.responses(...)`, and `OpenAI.apis.chat(...)` are fine. External provider packages should default-export the `Provider.make(...)` result and may add named aliases if useful. +`Provider.make(...)` remains available for simple static provider definitions, but new built-in providers should prefer plain configured facades unless a helper removes real duplication without adding runtime behavior. ### Folder layout @@ -113,7 +119,7 @@ Built-in providers are namespace modules from `src/providers/index.ts`, so alias packages/llm/src/ schema/ canonical Schema model, split by concern ids.ts branded IDs, literal types, ProviderMetadata - options.ts Generation/Provider/Http options, Capabilities, Limits, ModelRef + options.ts Generation/Provider/Http options, Limits, Model, cache policy messages.ts content parts, Message, ToolDefinition, LLMRequest events.ts Usage, individual events, LLMEvent, PreparedRequest, LLMResponse errors.ts error reasons, LLMError, ToolFailure @@ -145,12 +151,12 @@ packages/llm/src/ providers/ openai-compatible.ts generic compatible helper + family model helpers openai-compatible-profile.ts family defaults (deepseek, togetherai, ...) - azure.ts / amazon-bedrock.ts / github-copilot.ts / google.ts / xai.ts / openai.ts / anthropic.ts / openrouter.ts + azure.ts / amazon-bedrock.ts / cloudflare.ts / github-copilot.ts / google.ts / xai.ts / openai.ts / anthropic.ts / openrouter.ts tool.ts typed tool() helper tool-runtime.ts implementation helpers for LLMClient tool execution ``` -The dependency arrow points down: `providers/*.ts` files import `protocols`, `endpoint`, `auth`, and `framing`; protocols do not import provider metadata. Lower-level modules know nothing about specific providers. +The dependency arrow points down: `providers/*.ts` files import protocol routes and auth-option utilities; protocol modules import `endpoint`, `auth`, `framing`, and transport pieces. Protocols do not import provider facades. Lower-level modules know nothing about provider catalog metadata. ### Shared protocol helpers @@ -245,14 +251,14 @@ Use this order for every protocol module: 5. Request body construction (`fromRequest`) 6. Stream parsing (`step` and per-event handlers) 7. Protocol and route -8. Model helper +8. Protocol route export ### Rules - Keep protocol files focused on the protocol. Move provider-specific projection, signing, media normalization, or other bulky transformations into `src/protocols/utils/*`. - Use `Effect.fn("Provider.fromRequest")` for request body construction entrypoints. Use `Effect.fn(...)` for event handlers that yield effects; keep purely synchronous handlers as plain functions returning a `StepResult` that the dispatcher lifts via `Effect.succeed(...)`. -- Parser state owns terminal information. The state machine records finish reason, usage, and pending tool calls; emit one terminal `request-finish` (or `provider-error`) when a `terminal` event arrives. If a provider splits reason and usage across events, merge them in parser state before flushing. -- Emit exactly one terminal `request-finish` event for a completed response. Use `stream.terminal` to signal the run is over and have `step` emit the final event. +- Parser state owns terminal information. The state machine records finish reason, usage, and pending tool calls; emit one terminal `finish` event (or `provider-error`) for each completed response. If a provider splits reason and usage across events, merge them in parser state before flushing. +- Emit exactly one terminal `finish` event for a completed response, normally after a matching `step-finish`. Use `stream.terminal` to stop reading when the provider has a completion sentinel; use `stream.onHalt` when the final event must be flushed after the framed stream ends. - Use shared helpers for repeated protocol policy such as text joining, usage totals, JSON parsing, and tool-call accumulation. `ToolStream` (`protocols/utils/tool-stream.ts`) accumulates streamed tool-call arguments uniformly. - Make intentional provider differences explicit in helper names or comments. If two protocol files differ visually, the reason should be obvious from the names. - Prefer dispatched per-event handlers (`onMessageStart`, `onContentBlockDelta`, ...) called from a small top-level `step` switch over a long if-chain. The dispatcher keeps the event surface visible at a glance. diff --git a/packages/llm/README.md b/packages/llm/README.md index 321bf715bb1..020198dd64c 100644 --- a/packages/llm/README.md +++ b/packages/llm/README.md @@ -7,7 +7,7 @@ import { Effect } from "effect" import { LLM, LLMClient } from "@opencode-ai/llm" import { OpenAI } from "@opencode-ai/llm/providers" -const model = OpenAI.model("gpt-4o-mini", { apiKey: process.env.OPENAI_API_KEY }) +const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini") const request = LLM.request({ model, @@ -28,10 +28,10 @@ Run `LLMClient.stream(request)` instead of `generate` when you want incremental - **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes. - **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use. -- **`LLM.user(...)` / `LLM.assistant(...)` / `LLM.toolMessage(...)`** — message constructors. -- **`LLM.toolCall(...)` / `LLM.toolResult(...)` / `LLM.toolDefinition(...)`** — tool-related parts. +- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model. +- **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model. - **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing. -- **`LLMEvent.is.*`** — typed guards (`is.text`, `is.toolCall`, `is.requestFinish`, …) for filtering streams. +- **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams. ## Caching @@ -92,17 +92,19 @@ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` a ## Providers -Each provider exports a `model(...)` helper that records identity, protocol, capabilities, auth, and defaults. +Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime. ```ts -import { Anthropic } from "@opencode-ai/llm/providers" +import { OpenAI, CloudflareAIGateway } from "@opencode-ai/llm/providers" -const model = Anthropic.model("claude-sonnet-4-6", { - apiKey: process.env.ANTHROPIC_API_KEY, -}) +const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini") +const gateway = CloudflareAIGateway.configure({ + accountId: process.env.CLOUDFLARE_ACCOUNT_ID, + gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN, +}).model("workers-ai/@cf/meta/llama-3.1-8b-instruct") ``` -Included providers: OpenAI, Anthropic, Google (Gemini), Amazon Bedrock, Azure OpenAI, Cloudflare, GitHub Copilot, OpenRouter, xAI, plus generic OpenAI-compatible helpers for DeepSeek, Cerebras, Groq, Fireworks, Together, etc. +Included providers: OpenAI, Anthropic, Google (Gemini), Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, plus generic OpenAI-compatible helpers for DeepSeek, Cerebras, Groq, Fireworks, Together, etc. ## Provider options & HTTP overlays @@ -112,15 +114,15 @@ Three escape hatches in order of stability: 2. **`providerOptions: { : {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). 3. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist. -Model-level defaults are overridden by request-level values for each axis. +Route/provider defaults are overridden by request-level values for each axis. ## Routes -Adding a new model or deployment is usually 5–15 lines using `Route.make({ protocol, transport, ... })`. The four orthogonal pieces are protocol (body construction + stream parsing), transport (endpoint + auth + framing + encoding), defaults, and capabilities. See `AGENTS.md` for the architectural detail. +Adding a new model or deployment is usually 5-15 lines using `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports are reusable IO templates that receive route endpoint/auth at compile time. Capability/catalog metadata lives outside this low-level package; unsupported request shapes fail during protocol lowering. See `AGENTS.md` for the architectural detail. ## Effect -This package is built on Effect. Public methods return `Effect` or `Stream`; provide `LLMClient.layer` (the default registers every shipped route) for runtime dispatch. The example at `example/tutorial.ts` is a runnable walkthrough. +This package is built on Effect. Public methods return `Effect` or `Stream`; provide `LLMClient.layer` for runtime dispatch and import the provider/protocol modules for the routes you use. The example at `example/tutorial.ts` is a runnable walkthrough. ## See also diff --git a/packages/llm/example/call-sites.md b/packages/llm/example/call-sites.md new file mode 100644 index 00000000000..093f74e51de --- /dev/null +++ b/packages/llm/example/call-sites.md @@ -0,0 +1,591 @@ +# LLM Call Site Sketches + +Scratchpad for examples first, abstractions second. Current direction: routes +execute, provider facades organize configured route sets, and models carry route +values directly. + +## Conversation Summary + +Kit and Aidan want provider-specific LLM behavior to move out of opencode's AI +SDK transform path and into `packages/llm` where possible. The goal is not a big +generic transform layer; the goal is small composable route definitions backed by +recorded golden tests. + +Things to keep testing against: + +- Cache placement: `cache: "auto"`, manual cache breakpoints, provider cache usage. +- Images: golden image tests for providers/protocols that claim image support. +- Reasoning: canonical reasoning parts/events versus provider-native knobs. +- Auth: bearer, custom headers, multiple credentials, query auth, SigV4, OAuth, no auth. +- OpenAI-compatible providers: DeepSeek, Together, Groq, Alibaba/DashScope, custom routers. +- Provider switching: stale signatures, encrypted reasoning, provider metadata, incompatible parts. +- Error quality: typed errors instead of generic SDK/server failures. + +## Final Guide: Routes Execute, Providers Organize + +Do not introduce a first-class `Deployment` abstraction unless it gains real +semantics. Provider facades are ergonomic configured route groups, not execution +registries. The executable/composable thing is still a route. Do not make route +construction publish to a global registry; models should carry their route value +directly. + +Keep durable identity separate from runtime capability: + +- Durable identity is small serializable data like `{ providerID, modelID }` for + config, sessions, logs, and catalogs. +- Runtime capability is a `Model` with a route value, protocol, transport, auth, + and defaults. It is allowed to contain functions and schemas. +- If persisted identity needs to become executable, resolve it through an app + boundary first. Do not make `LLMRequest` recover behavior from a global route + side table. + +Keep unconfigured behavior values as values, not factories. A transport like +`HttpTransport.sseJson` should be a reusable immutable value. Use a function only +when the caller supplies options or when construction needs fresh state. + +Use constants to remove repetition before inventing abstractions. Provider ids +are branded once per provider facade and reused across routes; a plain exported +object is enough for the provider-facing API unless a helper earns its keep by +removing repeated route projection. + +Expose default configured provider instances, and put provider-specific setup on +`.configure(...)`. Model selectors stay pure: `model(id)`, `responses(id)`, +`chat(id)`, etc. Endpoint/auth/resource/api-version configuration happens before +model selection, not as a second argument to model selection. + +Use provider/product facades consistently: + +- One coherent provider/product config surface gets one top-level facade. +- APIs/model kinds that share that config are methods on the facade. +- Different products with different required config get separate top-level + facades, not a shared namespace with unrelated children. +- Default facades are exposed only when concrete defaults or lazy env/credential + defaults make the facade valid. + +Examples: + +```ts +OpenAI.responses("gpt-4o") +OpenAI.chat("gpt-4o") +OpenAI.responsesWebSocket("gpt-4o") + +Azure.configure({ resourceName, apiKey }).responses("my-deployment") +AmazonBedrock.configure({ region, credentials }).model("anthropic.claude-3-5-sonnet-20241022-v2:0") + +CloudflareAIGateway.configure({ accountId, gatewayId, gatewayApiKey, apiKey }).model("openai/gpt-4o") +CloudflareWorkersAI.configure({ accountId, apiKey }).model("@cf/meta/llama-3.1-8b-instruct") + +OpenAICompatible.configure({ + provider: "custom", + baseURL: "https://custom.example/v1", + auth: Auth.bearer(apiKey), +}).model("custom-model") +``` + +Standardize the provider facade contract before abstracting construction. A +plain object is enough at first; add a helper only if repeated route projection +starts hiding the real provider-specific config. + +`Route.with(...)` patch semantics should be boring and explicit: + +- Omitted fields inherit from the original route. +- `endpoint` patches merge with the existing endpoint, so overriding `baseURL` + keeps the existing `path`. +- `endpoint.query` merges by default; later values win. +- `auth` replaces. +- `headers` merge by default; undefined values are omitted. +- `id` is optional in patches. Route ids are diagnostic/provider API labels, not + global runtime registry keys. + +1. **Route** + - route id + - provider id + - protocol + - body schema + - body builder + - stream event schema + - parser/state machine + - transport + - method / IO shape + - framing + - request preparation + - constants when unconfigured; functions only when configured + - endpoint + - base URL + - static path + - body/model-derived path + - query params + - auth + - bearer + - custom header + - multiple credentials + - SigV4 + - none + - defaults + - headers + - generation defaults + - provider options + - limits +2. **Provider Facade** + - default configured provider instance + - provider-specific `.configure(...)` + - plain object/function facade over one or more routes + - top-level export only when it represents one coherent config surface + - no passive `Provider.make(...)` wrapper unless it gains runtime behavior +3. **Model Selector** + - route/provider-owned selector + - accepts model id only + - returns executable models + - does not accept endpoint/auth/deployment overrides +4. **Model** + - model id + - route value + - provider id + - configured route value at selection time +5. **LLM Request** + - model + - messages/tools + - generation/cache/reasoning/response-format options + - request-level HTTP overlays for per-request headers/query/body additions, + not provider endpoint/auth reconfiguration +6. **Compile** + - read route from model + - merge route defaults and request overrides + - build final URL from route endpoint + - apply auth from the configured route + - build body with protocol + - execute with transport and parse with protocol + +## Provider Facade Shape + +The provider abstraction is a facade over configured routes, not the runtime +execution mechanism: + +```ts +type ProviderFacade = { + readonly id: ProviderID + readonly model: (id: string) => Model + readonly configure: (input?: Config) => ProviderFacade +} & APIs +``` + +Manual construction is fine and should be the default until duplication earns a +helper: + +```ts +export const OpenAI = { + id: openAIProvider, + model: openAIResponses.model, + responses: openAIResponses.model, + chat: openAIChat.model, + configure: configureOpenAI, +} satisfies ProviderFacade< + { + responses: (id: string) => Model + chat: (id: string) => Model + }, + OpenAIConfig +> +``` + +If several providers repeat the same projection from route values to model +methods, the helper can stay deliberately tiny: + +```ts +const configureOpenAI = (input: OpenAIConfig = {}) => + Provider.define({ + id: openAIProvider, + routes: { + responses: openAIResponses.with(openAIConfig(input)), + chat: openAIChat.with(openAIConfig(input)), + }, + default: "responses", + configure: configureOpenAI, + }) + +export const OpenAI = configureOpenAI() +``` + +`Provider.define(...)` would only project route methods and preserve types: + +```ts +OpenAI.model("gpt-4o") +OpenAI.responses("gpt-4o") +OpenAI.chat("gpt-4o") +OpenAI.configure({ apiKey }).responses("gpt-4o") +``` + +It must not register routes, select routes dynamically, or participate in +execution. Execution still reads the route value carried by the model. + +## Ideal Call Sites + +Define concrete routes for a native provider, then project them through a +provider facade: + +```ts +const openAIProvider = ProviderID.make("openai") + +const openAIResponses = Route.make({ + id: "openai-responses", + provider: openAIProvider, + protocol: OpenAIResponses.protocol, + transport: HttpTransport.sseJson, + endpoint: { + baseURL: "https://api.openai.com/v1", + path: "/responses", + }, + auth: Auth.envBearer("OPENAI_API_KEY"), +}) + +const openAIChat = Route.make({ + id: "openai-chat", + provider: openAIProvider, + protocol: OpenAIChat.protocol, + transport: HttpTransport.sseJson, + endpoint: { + baseURL: "https://api.openai.com/v1", + path: "/chat/completions", + }, + auth: Auth.envBearer("OPENAI_API_KEY"), +}) + +const openAIResponsesWebSocket = openAIResponses.with({ + id: "openai-responses-websocket", + transport: WebSocketTransport.json, +}) + +const openAIConfig = (input: OpenAIConfig) => ({ + endpoint: input.endpoint, + auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined), + headers: { + "OpenAI-Organization": input.organization, + "OpenAI-Project": input.project, + }, +}) + +const configureOpenAI = (input: OpenAIConfig = {}) => { + const responses = openAIResponses.with(openAIConfig(input)) + const responsesWebSocket = openAIResponsesWebSocket.with(openAIConfig(input)) + const chat = openAIChat.with(openAIConfig(input)) + + return { + id: openAIProvider, + responses: responses.model, + responsesWebSocket: responsesWebSocket.model, + chat: chat.model, + model: responses.model, + configure: configureOpenAI, + } +} + +export const OpenAI = configureOpenAI() +``` + +Specialize it functionally for concrete providers: + +```ts +const deepSeekProvider = ProviderID.make("deepseek") + +const deepseekChat = openAIChat.with({ + id: "deepseek-chat", + provider: deepSeekProvider, + endpoint: { + baseURL: "https://api.deepseek.com/v1", + }, + auth: Auth.envBearer("DEEPSEEK_API_KEY"), +}) + +const configureDeepSeek = (input: OpenAICompatibleConfig = {}) => { + const route = deepseekChat.with({ + endpoint: input.endpoint, + auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined), + }) + + return { + id: deepSeekProvider, + model: route.model, + configure: configureDeepSeek, + } +} + +export const DeepSeek = { + id: deepSeekProvider, + model: deepseekChat.model, + configure: configureDeepSeek, +} +``` + +Provider-specific configuration happens before model selection: + +```ts +const deepseek = DeepSeek.configure({ + endpoint: { + baseURL: "https://proxy.example.com/v1", + }, + auth: Auth.bearer(apiKey), +}) + +const model = deepseek.model("deepseek-chat") +``` + +Final request call site stays boring: + +```ts +const response = + yield * + LLM.generate( + LLM.request({ + model: DeepSeek.model("deepseek-chat"), + prompt: "Hello.", + }), + ) +``` + +HTTP versus WebSocket is represented as named route selectors, not as model or +request overrides. Same protocol, different transport, different route: + +```ts +OpenAI.responses("gpt-4o") +OpenAI.responsesWebSocket("gpt-4o") +``` + +The client should not require a different public layer just because a selected +route uses WebSocket. Use one `LLMClient.layer` with HTTP and WebSocket runtime +capabilities available; routes that do not need WebSocket simply never touch it. +If a WebSocket route is selected in an environment without WebSocket support, +fail with a typed transport configuration error. + +Azure is a route specialization with auth/path/default changes plus input +mapping. The public API configures the Azure resource once, then selects +deployment ids with pure model selectors: + +```ts +const azureProvider = ProviderID.make("azure") + +const azureResponses = openAIResponses.with({ + id: "azure-openai-responses", + provider: azureProvider, + auth: Auth.envHeader("api-key", "AZURE_OPENAI_API_KEY"), +}) + +const configureAzure = (input: AzureConfig = {}) => { + const route = azureResponses.with({ + endpoint: { + baseURL: + input.baseURL ?? + Endpoint.envBaseURL( + "AZURE_RESOURCE_NAME", + (resourceName) => `https://${resourceName}.openai.azure.com/openai/v1`, + ), + query: { "api-version": input.apiVersion ?? "v1" }, + }, + auth: input.apiKey ? Auth.header("api-key", input.apiKey) : Auth.envHeader("api-key", "AZURE_OPENAI_API_KEY"), + }) + + return { + id: azureProvider, + model: route.model, + responses: route.model, + configure: configureAzure, + } +} + +export const Azure = configureAzure() + +const azure = Azure.configure({ + resourceName: "my-resource", + apiVersion: "v1", +}) + +const model = azure.responses("my-deployment") +``` + +Default provider facades are only valid when required configuration has a lazy +default source. `Azure.responses("my-deployment")` can be valid if endpoint +resolution reads `AZURE_RESOURCE_NAME` lazily and fails with a typed +configuration error when missing. If a provider has no sensible lazy default, +do not expose a default model selector; expose only a configured entrypoint. + +Cloudflare AI Gateway and Workers AI are separate product facades because their +configuration surfaces differ. Do not make a root `Cloudflare.configure(...)` +pretend there is one coherent Cloudflare provider configuration: + +```ts +const cloudflareProvider = ProviderID.make("cloudflare-ai-gateway") + +const cloudflareOpenAIChat = openAIChat.with({ + id: "cloudflare-ai-gateway-openai-chat", + provider: cloudflareProvider, + auth: Auth.bearerHeader("cf-aig-authorization").andThen(Auth.bearer()), +}) + +const configureCloudflareAIGateway = (input: CloudflareAIGatewayConfig) => { + const route = cloudflareOpenAIChat.with({ + endpoint: { + baseURL: `https://gateway.ai.cloudflare.com/v1/${input.accountId}/${input.gatewayId}/openai`, + }, + auth: Auth.bearerHeader("cf-aig-authorization", input.gatewayApiKey).andThen(Auth.bearer(input.apiKey)), + }) + + return { + id: cloudflareProvider, + model: (modelID: string) => route.model({ id: modelID }), + configure: configureCloudflareAIGateway, + } +} + +export const CloudflareAIGateway = { + id: cloudflareProvider, + configure: configureCloudflareAIGateway, +} + +const gateway = CloudflareAIGateway.configure({ + accountId: "account", + gatewayId: "gateway", + gatewayApiKey, + apiKey, +}) + +const model = gateway.model("openai/gpt-4o") +``` + +If a Cloudflare product gains a full lazy env default, it can expose a direct +selector too. Until then, omitting `CloudflareAIGateway.model(...)` makes missing +account/gateway configuration unrepresentable. + +opencode's dynamic runtime should construct executable models at its app +boundary instead of exposing a giant unstructured public model constructor or a +generic dynamic resolver: + +```ts +const model = + providerID === "azure" + ? Azure.configure(resolvedAzureConfig).responses(apiModelID) + : endpoint.websocket + ? OpenAI.responsesWebSocket(apiModelID) + : OpenAI.responses(apiModelID) +``` + +That boundary can branch on durable config/catalog metadata and call typed +provider APIs directly. Transport selection belongs there too: map metadata like +`endpoint.websocket` to `OpenAI.responsesWebSocket(apiModelID)`; otherwise use +the normal `OpenAI.responses(apiModelID)` route. The client runtime only executes +the route carried by the model. + +## Competitive Shape + +This follows the strongest parts of adjacent libraries: + +- AI SDK: configured provider instances expose provider-specific model methods. +- Effect AI: executable models carry provider requirements and can be resolved by + an app boundary. +- LiteLLM/opencode config: dynamic `providerID/modelID` branching belongs at the + app boundary, not in the typed public provider API or a global runtime + resolver. +- LangChain/LlamaIndex: constructor-style config plus model id is convenient, + but we avoid making model selection also configure endpoint/auth. + +The chosen split is: + +```txt +Route = execution mechanics +Provider facade = configured route group +Model = selected executable model carrying route value +App boundary = explicit durable-config -> typed-provider call +``` + +## What This Removes + +- No `Provider.make(...)` as a core abstraction. +- No `Provider.make(...)` wrapper just to bind an id to model functions. Use a + branded provider id constant and a plain exported provider facade. +- No `Deployment.define(...)` unless future examples force it. +- No global route registry as the normal execution path. +- No import side effects required before a model can execute. +- No duplicate `provider.id` object when selected models already carry provider + id. +- No `model(id, overrides)` escape hatch. Model selection takes the model id; + endpoint/auth/deployment customization happens by configuring the route first. +- No transport override on model/request. HTTP SSE versus WebSocket is a named + route selector such as `responses` versus `responsesWebSocket`. +- No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one + client layer with the available transport capabilities. +- No executable `ModelRef`. The executable handle is `Model`; durable model + identity stays separate and cannot execute on its own. + +## Implementation Todo + +- [x] Replace the current executable `ModelRef` with `Model`. +- [x] Change `Model.route` to carry a route value, not a `RouteID` string. +- [ ] Keep a separate durable model identity type for persisted/session/catalog + data, likely `{ providerID, modelID }`, and make it clear that it cannot + execute without resolver context. +- [x] Change route model selectors so `route.model(id)` returns an executable + model with the route value attached, not a globally registered route id. +- [x] Remove the standalone `Route.model(route, defaults, mapInput)` helper; + configured route instances own model selection. +- [x] Remove endpoint/auth escape hatches from route model selection; callers must + configure endpoint/auth through `route.with(...)` or provider facades before + calling `.model(...)`. +- [x] Remove request-shaping defaults from `Model`; selected models now carry only + id, provider, and configured route while defaults live on routes or requests. +- [x] Rework `LLMClient.prepare` / `stream` / `generate` to read + `request.model.route` directly instead of calling `registeredRoute(...)`. +- [x] Remove `Route.make(...)` global registration from the normal execution + path; keep route ids only as diagnostics/provider API labels. +- [x] Model endpoint as `{ baseURL, path, query }` on routes, then remove the + current split where host/query live on the model and path lives in route + transport setup. +- [x] Define `Route.with(...)` with explicit patch semantics for endpoint merge, + query merge, header merge, auth replacement, and optional diagnostic id. +- [x] Make unconfigured transports reusable constants such as + `HttpTransport.sseJson`; keep transport functions only for configured/fresh + state construction. +- [x] Collapse the public WebSocket runtime split so one `LLMClient.layer` + exposes available transport capabilities and selected routes fail with typed + transport config errors when a required capability is missing. +- [x] Convert OpenAI provider APIs to provider-facade shape: + `OpenAI.configure(config).responses(id)`, `.chat(id)`, and + `.responsesWebSocket(id)`. +- [x] Convert Azure to a configured facade where resource/base URL/api version + setup happens before selecting deployment ids. +- [x] Split Cloudflare products into separate facades such as + `CloudflareAIGateway` and `CloudflareWorkersAI`; do not expose a shared root + config surface unless one product actually exists. +- [x] Migrate remaining built-in provider facades one at a time so configuration + happens before model selection and selectors accept only ids: + xAI, GitHub Copilot, OpenRouter, OpenAI-compatible families, Anthropic, + Google/Gemini, and Amazon Bedrock now use configured facades such as + `Provider.configure(options).model(id)` with named selectors where needed. +- [ ] Decide whether a tiny `Provider.define(...)` helper is warranted after two + or three provider conversions; start with plain objects if duplication is not + yet painful. +- [x] Update `packages/opencode/src/session/llm/native-request.ts` to construct + executable models at the session boundary with explicit provider facade + calls, mapping catalog metadata such as `endpoint.websocket` to the correct + named route selector. +- [ ] Update tests so direct route/provider tests assert route values are carried + by executable models, and opencode/native tests assert boundary-based route + selection. +- [ ] Remove compatibility exports or stale docs only after internal call sites + are migrated; do not keep duplicate constructor paths without an external + compatibility need. + +## Open Questions + +- Default facades with required setup: should providers like Azure and Bedrock + expose default model selectors only when all required setup has lazy env or + credential-chain defaults? If not, omit the default selector so missing config + is impossible at the type/API level. +- Lazy endpoint/auth values: should `Endpoint.envBaseURL(...)` and env-backed + auth produce typed configuration/authentication errors at compile/prepare time + or only when executing the transport? +- `Route.with(...)` clearing semantics: endpoint/query/header patches merge by + default, but what is the explicit way to remove an inherited value? +- Provider facade helper: keep plain objects until duplication hurts, or add a + tiny `Provider.define(...)` immediately to enforce shape and method projection? +- Auth shape: should auth stay as today's composable `Auth`, or split into an + auth placement/strategy and credential sources? +- Naming: is `baseURL` still the right endpoint field name, or should it be + `origin` / `urlPrefix` to clarify that route `path` is appended? diff --git a/packages/llm/example/tutorial.ts b/packages/llm/example/tutorial.ts index 37d747dd8d3..50b925b8757 100644 --- a/packages/llm/example/tutorial.ts +++ b/packages/llm/example/tutorial.ts @@ -1,6 +1,6 @@ import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect" -import { LLM, LLMClient, Provider, ProviderID, Tool, type ProviderModelOptions } from "@opencode-ai/llm" -import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode-ai/llm/route" +import { LLM, LLMClient, ProviderID, Tool } from "@opencode-ai/llm" +import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" import { OpenAI } from "@opencode-ai/llm/providers" /** @@ -18,18 +18,18 @@ const apiKey = Config.redacted("OPENAI_API_KEY") // 1. Pick a model. The provider helper records provider identity, protocol // choice, capabilities, deployment options, authentication, and defaults. -const model = OpenAI.model("gpt-4o-mini", { +const model = OpenAI.configure({ apiKey, generation: { maxTokens: 160 }, providerOptions: { openai: { store: false }, }, -}) +}).model("gpt-4o-mini") // 2. Build a provider-neutral request. This is useful when reusing one request // across generate and stream examples. // -// Options can live on both the model and the request: +// Options can live on both the configured route/provider facade and the request: // // - `generation`: common controls such as max tokens, temperature, topP/topK, // penalties, seed, and stop sequences. @@ -39,7 +39,7 @@ const model = OpenAI.model("gpt-4o-mini", { // - `http`: last-resort serializable overlays for final request body, headers, // and query params. Prefer typed `providerOptions` when a field is stable. // -// Model options are defaults. Request options override them for this call. +// Route/provider options are defaults. Request options override them for this call. const request = LLM.request({ model, system: "You are concise and practical.", @@ -193,19 +193,22 @@ const FakeProtocol = Protocol.make({ // axes that the protocol deliberately does not know: URL, auth, and framing. const FakeAdapter = Route.make({ id: "fake-echo", + provider: "fake-echo", protocol: FakeProtocol, - endpoint: Endpoint.path("/v1/echo"), + endpoint: Endpoint.path("/v1/echo", { baseURL: "https://fake.local" }), auth: Auth.passthrough, framing: Framing.sse, }) -// A provider module exports a Provider definition. The default `model` helper -// sets provider identity, protocol id, and the route id resolved by the registry. -const fakeEchoModel = Route.model(FakeAdapter, { provider: "fake-echo", baseURL: "https://fake.local" }) -const FakeEcho = Provider.make({ +// A provider module exports a configured facade. Configuration happens before +// model selection; model selectors accept ids only. +const FakeEcho = { id: ProviderID.make("fake-echo"), - model: (id: string, options: ProviderModelOptions = {}) => fakeEchoModel({ id, ...options }), -}) + configure: () => ({ + id: ProviderID.make("fake-echo"), + model: (id: string) => FakeAdapter.model({ id }), + }), +} // `LLMClient.prepare` is the lower-level inspection hook: it compiles through // body conversion, validation, endpoint, auth, and HTTP construction without @@ -213,7 +216,7 @@ const FakeEcho = Provider.make({ const inspectFakeProvider = Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: FakeEcho.model("tiny-echo"), + model: FakeEcho.configure().model("tiny-echo"), prompt: "Show me the provider pipeline.", }), ) @@ -227,7 +230,8 @@ const inspectFakeProvider = Effect.gen(function* () { // enabled at a time so the tutorial can demonstrate generate, prepare, stream, // or tool-loop behavior without spending tokens on every example. const requestExecutorLayer = RequestExecutor.defaultLayer -const llmClientLayer = LLMClient.layer.pipe(Layer.provide(requestExecutorLayer)) +const llmDeps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer) +const llmClientLayer = LLMClient.layer.pipe(Layer.provide(llmDeps)) const program = Effect.gen(function* () { // yield* generateOnce @@ -237,6 +241,6 @@ const program = Effect.gen(function* () { // yield* generateStructuredObject // yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object)))) yield* streamWithTools -}).pipe(Effect.provide(Layer.mergeAll(requestExecutorLayer, llmClientLayer))) +}).pipe(Effect.provide(Layer.mergeAll(llmDeps, llmClientLayer))) Effect.runPromise(program) diff --git a/packages/llm/package.json b/packages/llm/package.json index aed5676ec1f..109b2d76622 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.46", + "version": "7.3.54", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 6ab7a049fe5..60f96dc69aa 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -97,7 +97,7 @@ const markMessages = ( } export const applyCachePolicy = (request: LLMRequest): LLMRequest => { - if (!RESPECTS_INLINE_HINTS.has(request.model.route)) return request + if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request const policy = resolve(request.cache) if (!policy.tools && !policy.system && !policy.messages) return request diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index acf73b360e1..389bc263d22 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -1,4 +1,4 @@ -export { LLMClient, modelLimits, modelRef } from "./route/client" +export { LLMClient } from "./route/client" export { Auth } from "./route/auth" export { Provider } from "./provider" export type { @@ -6,7 +6,6 @@ export type { RouteRoutedModelInput, Interface as LLMClientShape, Service as LLMClientService, - ModelRefInput, } from "./route/client" export * from "./schema" export { Tool, ToolFailure, toDefinitions, tool } from "./tool" diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 6f6728216ba..33ec56f3e97 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -1,5 +1,5 @@ import { Effect, JsonSchema, Schema } from "effect" -import { LLMClient, modelLimits, modelRef, type ModelRefInput } from "./route/client" +import { LLMClient } from "./route/client" import { GenerationOptions, HttpOptions, @@ -9,16 +9,16 @@ import { LLMRequest, LLMResponse, Message, + type ModelInput as SchemaModelInput, SystemPart, ToolChoice, ToolDefinition, type ContentPart, - ToolCallPart, ToolResultPart, } from "./schema" import { make as makeTool, type ToolSchema } from "./tool" -export type ModelInput = ModelRefInput +export type ModelInput = SchemaModelInput export type MessageInput = Message.Input @@ -42,10 +42,6 @@ export type RequestInput = Omit< readonly http?: HttpOptions.Input } -export const limits = modelLimits - -export const model = modelRef - export const generate = LLMClient.generate export const stream = LLMClient.stream diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/llm/src/protocols/anthropic-messages.ts index e27af18426e..234ccd5baf0 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/llm/src/protocols/anthropic-messages.ts @@ -10,9 +10,11 @@ import { type CacheHint, type FinishReason, type LLMRequest, + type MediaPart, type ProviderMetadata, type ToolCallPart, type ToolDefinition, + type ToolResultContentPart, type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -39,6 +41,17 @@ const AnthropicTextBlock = Schema.Struct({ }) type AnthropicTextBlock = Schema.Schema.Type +const AnthropicImageBlock = Schema.Struct({ + type: Schema.tag("image"), + source: Schema.Struct({ + type: Schema.tag("base64"), + media_type: Schema.String, + data: Schema.String, + }), + cache_control: Schema.optional(AnthropicCacheControl), +}) +type AnthropicImageBlock = Schema.Schema.Type + const AnthropicThinkingBlock = Schema.Struct({ type: Schema.tag("thinking"), thinking: Schema.String, @@ -84,15 +97,24 @@ const AnthropicServerToolResultBlock = Schema.Struct({ }) type AnthropicServerToolResultBlock = Schema.Schema.Type +// Anthropic accepts either a plain string or an ordered array of text/image +// blocks inside `tool_result.content`. The array form is required when a tool +// returns image bytes (screenshot, image search, etc.) so they can be passed +// to the model as proper image inputs instead of being JSON-stringified into +// the prompt — which silently inflates context by megabytes and can push the +// conversation over the model's token limit. +const AnthropicToolResultContent = Schema.Union([AnthropicTextBlock, AnthropicImageBlock]) + const AnthropicToolResultBlock = Schema.Struct({ type: Schema.tag("tool_result"), tool_use_id: Schema.String, - content: Schema.String, + content: Schema.Union([Schema.String, Schema.Array(AnthropicToolResultContent)]), is_error: Schema.optional(Schema.Boolean), cache_control: Schema.optional(AnthropicCacheControl), }) -const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicToolResultBlock]) +const AnthropicUserBlock = Schema.Union([AnthropicTextBlock, AnthropicImageBlock, AnthropicToolResultBlock]) +type AnthropicUserBlock = Schema.Schema.Type const AnthropicAssistantBlock = Schema.Union([ AnthropicTextBlock, AnthropicThinkingBlock, @@ -184,7 +206,13 @@ const AnthropicEvent = Schema.Struct({ content_block: Schema.optional(AnthropicStreamBlock), delta: Schema.optional(AnthropicStreamDelta), usage: Schema.optional(AnthropicUsage), - error: Schema.optional(Schema.Struct({ type: Schema.String, message: Schema.String })), + // `type` and `message` are both required per Anthropic's spec, but + // OpenAI-compatible proxies and gateway translations occasionally drop one + // or the other; mark them optional so a partial payload still parses and + // the parser can fall back to whichever field is populated. + error: Schema.optional( + Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }), + ), }) type AnthropicEvent = Schema.Schema.Type @@ -272,6 +300,46 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult return { type: wireType, tool_use_id: part.id, content: part.result.value } satisfies AnthropicServerToolResultBlock }) +const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: MediaPart) { + if (!part.mediaType.startsWith("image/")) + return yield* invalid(`Anthropic Messages user media content only supports images`) + return { + type: "image" as const, + source: { + type: "base64" as const, + media_type: part.mediaType, + data: ProviderShared.mediaBase64(part), + }, + } satisfies AnthropicImageBlock +}) + +// Tool results may carry structured text/images. Keep media as provider-native +// content instead of JSON-stringifying base64 into a prompt string. +const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( + item: ToolResultContentPart, +) { + if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock + if (item.mediaType.startsWith("image/")) + return { + type: "image" as const, + source: { + type: "base64" as const, + media_type: item.mediaType, + data: ProviderShared.mediaBase64(item), + }, + } satisfies AnthropicImageBlock + return yield* invalid(`Anthropic Messages tool-result media content only supports images, got ${item.mediaType}`) +}) + +const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) { + // Text / json / error results stay as a string for backward compatibility + // with existing cassettes and provider expectations. + if (part.result.type !== "content") return ProviderShared.toolResultText(part) + // Preserve the narrowed array element type when compiled through a consumer package. + const content: ReadonlyArray = part.result.value + return yield* Effect.forEach(content, lowerToolResultContentItem) +}) + const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( request: LLMRequest, breakpoints: Cache.Breakpoints, @@ -280,11 +348,17 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( for (const message of request.messages) { if (message.role === "user") { - const content: AnthropicTextBlock[] = [] + const content: AnthropicUserBlock[] = [] for (const part of message.content) { - if (!ProviderShared.supportsContent(part, ["text"])) - return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text"]) - content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) }) + if (part.type === "text") { + content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) }) + continue + } + if (part.type === "media") { + content.push(yield* lowerImage(part)) + continue + } + return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"]) } messages.push({ role: "user", content }) continue @@ -328,7 +402,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( content.push({ type: "tool_result", tool_use_id: part.id, - content: ProviderShared.toolResultText(part), + content: yield* lowerToolResultContent(part), is_error: part.result.type === "error" ? true : undefined, cache_control: cacheControl(breakpoints, part.cache), }) @@ -386,7 +460,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques tools, tool_choice: toolChoice, stream: true as const, - max_tokens: generation?.maxTokens ?? request.model.limits.output ?? 4096, + max_tokens: generation?.maxTokens ?? request.model.route.defaults.limits?.output ?? 4096, temperature: generation?.temperature, top_p: generation?.topP, top_k: generation?.topK, @@ -452,8 +526,8 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => { totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined), providerMetadata: { anthropic: { - ...(left.providerMetadata?.["anthropic"] ?? {}), - ...(right.providerMetadata?.["anthropic"] ?? {}), + ...left.providerMetadata?.["anthropic"], + ...right.providerMetadata?.["anthropic"], }, }, }) @@ -635,9 +709,18 @@ const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult = return [{ ...state, lifecycle, usage }, events] } +// Prefix `error.type` so overloads, rate limits, and quota errors are visible +// even when the provider message is generic or empty. +const providerErrorMessage = (event: AnthropicEvent): string => { + const type = event.error?.type + const message = event.error?.message + if (type && message) return `${type}: ${message}` + return message || type || "Anthropic Messages stream error" +} + const onError = (state: ParserState, event: AnthropicEvent): StepResult => [ state, - [LLMEvent.providerError({ message: event.error?.message ?? "Anthropic Messages stream error" })], + [LLMEvent.providerError({ message: providerErrorMessage(event) })], ] const step = (state: ParserState, event: AnthropicEvent) => { @@ -673,19 +756,12 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, + provider: "anthropic", protocol, - endpoint: Endpoint.path(PATH), - auth: Auth.apiKeyHeader("x-api-key"), + endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), + auth: Auth.none, framing: Framing.sse, headers: () => ({ "anthropic-version": "2023-06-01" }), }) -// ============================================================================= -// Model Helper -// ============================================================================= -export const model = Route.model(route, { - provider: "anthropic", - baseURL: DEFAULT_BASE_URL, -}) - export * as AnthropicMessages from "./anthropic-messages" diff --git a/packages/llm/src/protocols/bedrock-converse.ts b/packages/llm/src/protocols/bedrock-converse.ts index 7f5647c4a7b..54eb7930f89 100644 --- a/packages/llm/src/protocols/bedrock-converse.ts +++ b/packages/llm/src/protocols/bedrock-converse.ts @@ -1,5 +1,5 @@ import { Effect, Schema } from "effect" -import { Route, type RouteModelInput } from "../route/client" +import { Route } from "../route/client" import { Endpoint } from "../route/endpoint" import { Protocol } from "../route/protocol" import { @@ -14,7 +14,7 @@ import { } from "../schema" import { BedrockEventStream } from "./bedrock-event-stream" import { JsonObject, optionalArray, ProviderShared } from "./shared" -import { BedrockAuth, type Credentials as BedrockCredentials } from "./utils/bedrock-auth" +import { BedrockAuth } from "./utils/bedrock-auth" import { BedrockCache } from "./utils/bedrock-cache" import { BedrockMedia } from "./utils/bedrock-media" import { Lifecycle } from "./utils/lifecycle" @@ -24,23 +24,6 @@ const ADAPTER = "bedrock-converse" export type { Credentials as BedrockCredentials } from "./utils/bedrock-auth" -// ============================================================================= -// Public Model Input -// ============================================================================= -export type BedrockConverseModelInput = RouteModelInput & { - /** - * Bearer API key (Bedrock's newer API key auth). Sets the `Authorization` - * header and bypasses SigV4 signing. Mutually exclusive with `credentials`. - */ - readonly apiKey?: string - /** - * AWS credentials for SigV4 signing. The route signs each request at - * `toHttp` time using `aws4fetch`. Mutually exclusive with `apiKey`. - */ - readonly credentials?: BedrockCredentials - readonly headers?: Record -} - // ============================================================================= // Request Body Schema // ============================================================================= @@ -61,6 +44,7 @@ type BedrockToolUseBlock = Schema.Schema.Type const BedrockToolResultContentItem = Schema.Union([ Schema.Struct({ text: Schema.String }), Schema.Struct({ json: Schema.Unknown }), + BedrockMedia.ImageBlock, ]) const BedrockToolResultBlock = Schema.Struct({ @@ -261,15 +245,33 @@ const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({ }, }) -const lowerToolResult = (part: ToolResultPart): BedrockToolResultBlock => ({ - toolResult: { - toolUseId: part.id, - content: - part.result.type === "text" || part.result.type === "error" - ? [{ text: ProviderShared.toolResultText(part) }] - : [{ json: part.result.value }], - status: part.result.type === "error" ? "error" : "success", - }, +const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (part: ToolResultPart) { + if (part.result.type === "text" || part.result.type === "error") + return [{ text: ProviderShared.toolResultText(part) }] + if (part.result.type === "json") return [{ json: part.result.value }] + + const content: Array> = [] + for (const item of part.result.value) { + if (item.type === "text") { + content.push({ text: item.text }) + continue + } + const media = yield* BedrockMedia.lower(item) + if (!("image" in media)) + return yield* ProviderShared.invalidRequest("Bedrock Converse only supports image media in tool results") + content.push(media) + } + return content +}) + +const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (part: ToolResultPart) { + return { + toolResult: { + toolUseId: part.id, + content: yield* lowerToolResultContent(part), + status: part.result.type === "error" ? "error" : "success", + }, + } satisfies BedrockToolResultBlock }) const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* ( @@ -331,7 +333,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* ( for (const part of message.content) { if (!ProviderShared.supportsContent(part, ["tool-result"])) return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"]) - content.push(lowerToolResult(part)) + content.push(yield* lowerToolResult(part)) const cachePoint = BedrockCache.block(breakpoints, part.cache) if (cachePoint) content.push(cachePoint) } @@ -597,11 +599,11 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, + provider: "bedrock", protocol, - // Bedrock's URL embeds the region in the host (set on `model.baseURL` by - // the provider helper from credentials) and the validated modelId in the - // path. We read the validated body so the URL matches the body that gets - // signed. + // Bedrock's URL embeds the region in the route endpoint host and the + // validated modelId in the path. We read the validated body so the URL + // matches the body that gets signed. endpoint: Endpoint.path( ({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`, ), @@ -609,26 +611,6 @@ export const route = Route.make({ framing, }) -export const nativeCredentials = BedrockAuth.nativeCredentials - -const bedrockModel = Route.model( - route, - { - provider: "bedrock", - }, - { - mapInput: (input: BedrockConverseModelInput) => { - const { credentials, ...rest } = input - const region = credentials?.region ?? "us-east-1" - return { - ...rest, - baseURL: rest.baseURL ?? `https://bedrock-runtime.${region}.amazonaws.com`, - native: nativeCredentials(input.native, credentials), - } - }, - }, -) - -export const model = bedrockModel +export const sigV4Auth = BedrockAuth.sigV4 export * as BedrockConverse from "./bedrock-converse" diff --git a/packages/llm/src/protocols/gemini.ts b/packages/llm/src/protocols/gemini.ts index 6e0b82abba9..5fe4dcc760c 100644 --- a/packages/llm/src/protocols/gemini.ts +++ b/packages/llm/src/protocols/gemini.ts @@ -404,19 +404,14 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, + provider: "google", protocol, // Gemini's path embeds the model id and pins SSE framing at the URL level. - endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`), - auth: Auth.apiKeyHeader("x-goog-api-key"), + endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, { + baseURL: DEFAULT_BASE_URL, + }), + auth: Auth.none, framing: Framing.sse, }) -// ============================================================================= -// Model Helper -// ============================================================================= -export const model = Route.model(route, { - provider: "google", - baseURL: DEFAULT_BASE_URL, -}) - export * as Gemini from "./gemini" diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 470a1473c40..6a85c37d593 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -2,7 +2,6 @@ import { Array as Arr, Effect, Schema } from "effect" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" -import { Framing } from "../route/framing" import { HttpTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { @@ -128,6 +127,7 @@ type OpenAIChatToolCallDelta = Schema.Schema.Type let lifecycle = state.lifecycle + if (delta?.reasoning_content) + lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content) + if (delta?.content) lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content) for (const tool of toolDeltas) { @@ -393,28 +396,15 @@ export const protocol = Protocol.make({ }, }) -const encodeBody = Schema.encodeSync(Schema.fromJsonString(OpenAIChatBody)) - -export const httpTransport = HttpTransport.httpJson({ - endpoint: Endpoint.path(PATH), - auth: Auth.bearer(), - framing: Framing.sse, - encodeBody, -}) +export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", protocol, + endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), + auth: Auth.none, transport: httpTransport, - defaults: { - baseURL: DEFAULT_BASE_URL, - }, }) -// ============================================================================= -// Model Helper -// ============================================================================= -export const model = route.model - export * as OpenAIChat from "./openai-chat" diff --git a/packages/llm/src/protocols/openai-compatible-chat.ts b/packages/llm/src/protocols/openai-compatible-chat.ts index 76deeac4513..ce3f0a83d75 100644 --- a/packages/llm/src/protocols/openai-compatible-chat.ts +++ b/packages/llm/src/protocols/openai-compatible-chat.ts @@ -5,16 +5,14 @@ import * as OpenAIChat from "./openai-chat" const ADAPTER = "openai-compatible-chat" -export type OpenAICompatibleChatModelInput = Omit & { - readonly baseURL: string -} +export type OpenAICompatibleChatModelInput = RouteRoutedModelInput /** * Route for non-OpenAI providers that expose an OpenAI Chat-compatible * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and * overrides only the route id so providers can be resolved per-family without - * colliding with native OpenAI. The model carries the host on `baseURL`, - * supplied by whichever profile/provider helper builds it. + * colliding with native OpenAI. Provider helpers configure the route endpoint + * before model selection. */ export const route = Route.make({ id: ADAPTER, @@ -23,6 +21,4 @@ export const route = Route.make({ framing: Framing.sse, }) -export const model = Route.model(route) - export * as OpenAICompatibleChat from "./openai-compatible-chat" diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 7cf734f0278..ad673a263f4 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -2,7 +2,6 @@ import { Effect, Schema } from "effect" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" -import { Framing } from "../route/framing" import { HttpTransport, WebSocketTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { @@ -11,9 +10,12 @@ import { type FinishReason, type LLMRequest, type ProviderMetadata, + type ReasoningPart, type TextPart, type ToolCallPart, type ToolDefinition, + type ToolResultContentPart, + type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { OpenAIOptions } from "./utils/openai-options" @@ -31,16 +33,51 @@ const OpenAIResponsesInputText = Schema.Struct({ type: Schema.tag("input_text"), text: Schema.String, }) +const OpenAIResponsesInputImage = Schema.Struct({ + type: Schema.tag("input_image"), + image_url: Schema.String, +}) +const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) +type OpenAIResponsesInputContent = Schema.Schema.Type const OpenAIResponsesOutputText = Schema.Struct({ type: Schema.tag("output_text"), text: Schema.String, }) +const OpenAIResponsesReasoningSummaryText = Schema.Struct({ + type: Schema.tag("summary_text"), + text: Schema.String, +}) + +const OpenAIResponsesReasoningItem = Schema.Struct({ + type: Schema.tag("reasoning"), + id: Schema.String, + summary: Schema.Array(OpenAIResponsesReasoningSummaryText), + encrypted_content: optionalNull(Schema.String), +}) + +const OpenAIResponsesItemReference = Schema.Struct({ + type: Schema.tag("item_reference"), + id: Schema.String, +}) + +// `function_call_output.output` accepts either a plain string or an ordered +// array of content items so tools can return images in addition to text. +// https://platform.openai.com/docs/api-reference/responses/object +const OpenAIResponsesFunctionCallOutputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) + +const OpenAIResponsesFunctionCallOutput = Schema.Union([ + Schema.String, + Schema.Array(OpenAIResponsesFunctionCallOutputContent), +]) + const OpenAIResponsesInputItem = Schema.Union([ Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), - Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputText) }), + Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), + OpenAIResponsesReasoningItem, + OpenAIResponsesItemReference, Schema.Struct({ type: Schema.tag("function_call"), call_id: Schema.String, @@ -50,11 +87,20 @@ const OpenAIResponsesInputItem = Schema.Union([ Schema.Struct({ type: Schema.tag("function_call_output"), call_id: Schema.String, - output: Schema.String, + output: OpenAIResponsesFunctionCallOutput, }), ]) type OpenAIResponsesInputItem = Schema.Schema.Type +// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold +// multiple streamed summary parts into the same item before flushing. +type OpenAIResponsesReasoningInput = { + type: "reasoning" + id: string + summary: Array<{ type: "summary_text"; text: string }> + encrypted_content?: string | null +} + const OpenAIResponsesTool = Schema.Struct({ type: Schema.tag("function"), name: Schema.String, @@ -76,11 +122,12 @@ const OpenAIResponsesToolChoice = Schema.Union([ const OpenAIResponsesCoreFields = { model: Schema.String, input: Schema.Array(OpenAIResponsesInputItem), + instructions: Schema.optional(Schema.String), tools: optionalArray(OpenAIResponsesTool), tool_choice: Schema.optional(OpenAIResponsesToolChoice), store: Schema.optional(Schema.Boolean), prompt_cache_key: Schema.optional(Schema.String), - include: optionalArray(Schema.Literal("reasoning.encrypted_content")), + include: optionalArray(OpenAIOptions.OpenAIResponseIncludable), reasoning: Schema.optional( Schema.Struct({ effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), @@ -142,24 +189,42 @@ const OpenAIResponsesStreamItem = Schema.Struct({ server_label: Schema.optional(Schema.String), output: Schema.optional(Schema.Unknown), error: Schema.optional(Schema.Unknown), + encrypted_content: optionalNull(Schema.String), }) type OpenAIResponsesStreamItem = Schema.Schema.Type +// OpenAI Responses surfaces provider failures in two related shapes. The +// streaming `error` event carries the details at the top level +// (`{ type: "error", code, message, param, sequence_number }`), while +// `response.failed` carries them under `response.error`. We capture both so +// the parser can surface a useful provider-error message in either path. +const OpenAIResponsesErrorPayload = Schema.Struct({ + code: optionalNull(Schema.String), + message: optionalNull(Schema.String), + param: optionalNull(Schema.String), +}) + const OpenAIResponsesEvent = Schema.Struct({ type: Schema.String, delta: Schema.optional(Schema.String), item_id: Schema.optional(Schema.String), + summary_index: Schema.optional(Schema.Number), item: Schema.optional(OpenAIResponsesStreamItem), response: Schema.optional( - Schema.Struct({ - id: Schema.optional(Schema.String), - service_tier: Schema.optional(Schema.String), - incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), - usage: optionalNull(OpenAIResponsesUsage), - }), + Schema.StructWithRest( + Schema.Struct({ + id: Schema.optional(Schema.String), + service_tier: optionalNull(Schema.String), + incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), + usage: optionalNull(OpenAIResponsesUsage), + error: optionalNull(OpenAIResponsesErrorPayload), + }), + [Schema.Record(Schema.String, Schema.Unknown)], + ), ), code: Schema.optional(Schema.String), message: Schema.optional(Schema.String), + param: Schema.optional(Schema.String), }) type OpenAIResponsesEvent = Schema.Schema.Type @@ -167,6 +232,18 @@ interface ParserState { readonly tools: ToolStream.State readonly hasFunctionCall: boolean readonly lifecycle: Lifecycle.State + readonly reasoningItems: Readonly> + readonly store: boolean | undefined +} + +type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded" + +interface ReasoningStreamItem { + readonly encryptedContent: string | null | undefined + // Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to + // strings, but typing the map as `Record` documents intent + // and matches the wire field. + readonly summaryParts: Readonly> } const invalid = ProviderShared.invalidRequest @@ -196,50 +273,136 @@ const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({ arguments: ProviderShared.encodeJson(part.input), }) +const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => { + const openai = part.providerMetadata?.openai + if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0) + return undefined + const encryptedContent = + typeof openai.reasoningEncryptedContent === "string" + ? openai.reasoningEncryptedContent + : openai.reasoningEncryptedContent === null + ? null + : undefined + return { + type: "reasoning", + id: openai.itemId, + summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], + encrypted_content: encryptedContent, + } +} + +const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( + part: LLMRequest["messages"][number]["content"][number], +) { + if (part.type === "text") return { type: "input_text" as const, text: part.text } + if (part.type === "media" && part.mediaType.startsWith("image/")) { + return { type: "input_image" as const, image_url: ProviderShared.mediaDataUrl(part) } + } + if (part.type === "media") return yield* invalid("OpenAI Responses user media content only supports images") + return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) +}) + +// Tool results may carry structured text/images. Keep media as provider-native +// content instead of JSON-stringifying base64 into a prompt string. +const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* ( + item: ToolResultContentPart, +) { + if (item.type === "text") return { type: "input_text" as const, text: item.text } + if (item.mediaType.startsWith("image/")) + return { + type: "input_image" as const, + image_url: ProviderShared.mediaDataUrl(item), + } + return yield* invalid(`OpenAI Responses tool-result media content only supports images, got ${item.mediaType}`) +}) + +const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")(function* (part: ToolResultPart) { + // Text/json/error results are encoded as a plain string for backward + // compatibility with existing cassettes and provider expectations. + if (part.result.type !== "content") return ProviderShared.toolResultText(part) + return yield* Effect.forEach(part.result.value, lowerToolResultContentItem) +}) + const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { const system: OpenAIResponsesInputItem[] = request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] const input: OpenAIResponsesInputItem[] = [...system] + const store = OpenAIOptions.store(request) for (const message of request.messages) { if (message.role === "user") { - const content: TextPart[] = [] - for (const part of message.content) { - if (!ProviderShared.supportsContent(part, ["text"])) - return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text"]) - content.push(part) - } - input.push({ role: "user", content: content.map((part) => ({ type: "input_text", text: part.text })) }) + input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) continue } if (message.role === "assistant") { const content: TextPart[] = [] + const reasoningItems: Record = {} + const reasoningReferences = new Set() + const flushText = () => { + if (content.length === 0) return + input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + content.splice(0, content.length) + } for (const part of message.content) { - if (!ProviderShared.supportsContent(part, ["text", "tool-call"])) - return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", ["text", "tool-call"]) if (part.type === "text") { content.push(part) continue } + if (part.type === "reasoning") { + flushText() + const reasoning = lowerReasoning(part) + if (!reasoning) continue + if (store !== false && reasoning.id) { + if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id }) + reasoningReferences.add(reasoning.id) + continue + } + const existing = reasoningItems[reasoning.id] + if (existing) { + existing.summary.push(...reasoning.summary) + if (typeof reasoning.encrypted_content === "string") + existing.encrypted_content = reasoning.encrypted_content + continue + } + reasoningItems[reasoning.id] = reasoning + input.push(reasoning) + continue + } if (part.type === "tool-call") { + flushText() input.push(lowerToolCall(part)) continue } + return yield* ProviderShared.unsupportedContent("OpenAI Responses", "assistant", [ + "text", + "reasoning", + "tool-call", + ]) } - if (content.length > 0) - input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) + flushText() continue } for (const part of message.content) { if (!ProviderShared.supportsContent(part, ["tool-result"])) return yield* ProviderShared.unsupportedContent("OpenAI Responses", "tool", ["tool-result"]) - input.push({ type: "function_call_output", call_id: part.id, output: ProviderShared.toolResultText(part) }) + input.push({ + type: "function_call_output", + call_id: part.id, + output: yield* lowerToolResultOutput(part), + }) } } - return input + // With store:false, OpenAI only accepts previous reasoning items when the + // complete item has encrypted state. Summary blocks for one item may carry + // that state only on the last block, so filter after they have been joined. + return store === false + ? input.filter( + (item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string", + ) + : input }) const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) { @@ -249,12 +412,14 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques if (effort && !OpenAIOptions.isReasoningEffort(effort)) return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`) const summary = OpenAIOptions.reasoningSummary(request) - const encryptedState = OpenAIOptions.encryptedReasoning(request) + const include = OpenAIOptions.include(request) const verbosity = OpenAIOptions.textVerbosity(request) + const instructions = OpenAIOptions.instructions(request) return { + ...(instructions ? { instructions } : {}), ...(store !== undefined ? { store } : {}), ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}), - ...(encryptedState ? { include: ["reasoning.encrypted_content"] as const } : {}), + ...(include ? { include } : {}), ...(effort || summary ? { reasoning: { effort, summary } } : {}), ...(verbosity ? { text: { verbosity } } : {}), } @@ -345,6 +510,11 @@ const isHostedToolItem = ( ): item is OpenAIResponsesStreamItem & { type: HostedToolType; id: string } => item.type in HOSTED_TOOLS && typeof item.id === "string" && item.id.length > 0 +const isReasoningItem = ( + item: OpenAIResponsesStreamItem, +): item is OpenAIResponsesStreamItem & { type: "reasoning"; id: string } => + item.type === "reasoning" && typeof item.id === "string" && item.id.length > 0 + // Round-trip the full item as the structured result so consumers can extract // outputs / sources / status without re-decoding. const hostedToolResult = (item: OpenAIResponsesStreamItem) => { @@ -394,8 +564,54 @@ const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): Ste ] } +const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + if (!event.delta) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + const itemID = event.item_id ?? "reasoning-0" + const id = + event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID + return [ + { + ...state, + lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta), + }, + events, + ] +} + +const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS] + +const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => + openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null }) + +// OpenAI Responses streams reasoning items in a stable order: +// `output_item.added` (reasoning) → +// `reasoning_summary_part.added` (index=0) → +// `reasoning_summary_text.delta` → +// `reasoning_summary_part.done` (index=0) → +// (repeat for index>0) → +// `output_item.done` (reasoning). +// The handlers below rely on this ordering: `onOutputItemAdded` seeds the +// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0` +// short-circuits when the entry already exists, and higher-index handlers +// fold against the same entry. Behaviour for out-of-order events is +// best-effort, not guaranteed. const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { const item = event.item + if (item && isReasoningItem(item)) { + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)), + reasoningItems: { + ...state.reasoningItems, + [item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } }, + }, + }, + events, + ] + } if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS] const providerMetadata = openaiMetadata({ itemId: item.id }) const events: LLMEvent[] = [] @@ -416,6 +632,103 @@ const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): Ste ] } +const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} } + if (event.summary_index === 0) { + if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart( + state.lifecycle, + events, + `${event.item_id}:0`, + openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }), + ), + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { ...item, summaryParts: { 0: "active" } }, + }, + }, + events, + ] + } + + const events: LLMEvent[] = [] + const closed = Object.entries(item.summaryParts) + .filter((entry) => entry[1] === "can-conclude") + .reduce( + (lifecycle, entry) => + Lifecycle.reasoningEnd( + lifecycle, + events, + `${event.item_id}:${entry[0]}`, + openaiMetadata({ itemId: event.item_id }), + ), + state.lifecycle, + ) + return [ + { + ...state, + lifecycle: Lifecycle.reasoningStart( + closed, + events, + `${event.item_id}:${event.summary_index}`, + openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }), + ), + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { + ...item, + summaryParts: { + ...Object.fromEntries( + Object.entries(item.summaryParts).map((entry) => + entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry, + ), + ), + [event.summary_index]: "active", + }, + }, + }, + }, + events, + ] +} + +const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { + if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS] + const item = state.reasoningItems[event.item_id] + if (!item) return [state, NO_EVENTS] + const events: LLMEvent[] = [] + return [ + { + ...state, + lifecycle: + state.store !== false + ? Lifecycle.reasoningEnd( + state.lifecycle, + events, + `${event.item_id}:${event.summary_index}`, + openaiMetadata({ itemId: event.item_id }), + ) + : state.lifecycle, + reasoningItems: { + ...state.reasoningItems, + [event.item_id]: { + ...item, + summaryParts: { + ...item.summaryParts, + [event.summary_index]: state.store !== false ? "concluded" : "can-conclude", + }, + }, + }, + }, + events, + ] +} + const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* ( state: ParserState, event: OpenAIResponsesEvent, @@ -473,6 +786,32 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function* return [{ ...state, lifecycle }, events] satisfies StepResult } + if (isReasoningItem(item)) { + const events: LLMEvent[] = [] + const providerMetadata = reasoningMetadata(item) + const reasoningItem = state.reasoningItems[item.id] + if (reasoningItem) { + const lifecycle = Object.entries(reasoningItem.summaryParts) + .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") + .reduce( + (lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata), + state.lifecycle, + ) + const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems + return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult + } + if (!state.lifecycle.reasoning.has(item.id)) { + const lifecycle = Lifecycle.stepStart(state.lifecycle, events) + events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata })) + events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata })) + return [{ ...state, lifecycle }, events] satisfies StepResult + } + return [ + { ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, item.id, providerMetadata) }, + events, + ] satisfies StepResult + } + return [state, NO_EVENTS] satisfies StepResult }) @@ -492,18 +831,47 @@ const onResponseFinish = (state: ParserState, event: OpenAIResponsesEvent): Step return [{ ...state, lifecycle }, events] } +// Build a single human-readable message from whatever the provider supplied. +// When both code and message are present, prefix the code so consumers see +// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just +// the bare message — production rate limits and context-length failures used +// to be indistinguishable from generic stream drops. +const providerErrorMessage = (event: OpenAIResponsesEvent, fallback: string): string => { + const nested = event.response?.error ?? undefined + const message = event.message || nested?.message || undefined + const code = event.code || nested?.code || undefined + if (message && code) return `${code}: ${message}` + return message || code || fallback +} + const onResponseFailed = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ state, - [LLMEvent.providerError({ message: event.message ?? event.code ?? "OpenAI Responses response failed" })], + [LLMEvent.providerError({ message: providerErrorMessage(event, "OpenAI Responses response failed") })], ] const onError = (state: ParserState, event: OpenAIResponsesEvent): StepResult => [ state, - [LLMEvent.providerError({ message: event.message ?? event.code ?? "OpenAI Responses stream error" })], + [LLMEvent.providerError({ message: providerErrorMessage(event, "OpenAI Responses stream error") })], ] const step = (state: ParserState, event: OpenAIResponsesEvent) => { if (event.type === "response.output_text.delta") return Effect.succeed(onOutputTextDelta(state, event)) + if ( + event.type === "response.reasoning_text.delta" || + event.type === "response.reasoning_summary.delta" || + event.type === "response.reasoning_summary_text.delta" + ) + return Effect.succeed(onReasoningDelta(state, event)) + if ( + event.type === "response.reasoning_text.done" || + event.type === "response.reasoning_summary.done" || + event.type === "response.reasoning_summary_text.done" + ) + return Effect.succeed(onReasoningDone(state, event)) + if (event.type === "response.reasoning_summary_part.added") + return Effect.succeed(onReasoningSummaryPartAdded(state, event)) + if (event.type === "response.reasoning_summary_part.done") + return Effect.succeed(onReasoningSummaryPartDone(state, event)) if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event)) if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event) if (event.type === "response.output_item.done") return onOutputItemDone(state, event) @@ -530,33 +898,30 @@ export const protocol = Protocol.make({ }, stream: { event: Protocol.jsonEvent(OpenAIResponsesEvent), - initial: () => ({ hasFunctionCall: false, tools: ToolStream.empty(), lifecycle: Lifecycle.initial() }), + initial: (request) => ({ + hasFunctionCall: false, + tools: ToolStream.empty(), + lifecycle: Lifecycle.initial(), + reasoningItems: {}, + store: OpenAIOptions.store(request), + }), step, terminal: (event) => TERMINAL_TYPES.has(event.type), }, }) -const encodeBody = Schema.encodeSync(Schema.fromJsonString(OpenAIResponsesBody)) -const transportBase = { - endpoint: Endpoint.path(PATH), - auth: Auth.bearer(), - encodeBody, -} -const routeDefaults = { - baseURL: DEFAULT_BASE_URL, -} +const endpoint = Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }) +const auth = Auth.none -export const httpTransport = HttpTransport.httpJson({ - ...transportBase, - framing: Framing.sse, -}) +export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", protocol, + endpoint, + auth, transport: httpTransport, - defaults: routeDefaults, }) const decodeWebSocketMessage = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesWebSocketMessage)) @@ -569,8 +934,10 @@ const webSocketMessage = (body: OpenAIResponsesBody | Record) = return yield* decodeWebSocketMessage({ ...message, type: "response.create" }) }) -export const webSocketTransport = WebSocketTransport.json({ - ...transportBase, +export const webSocketTransport = WebSocketTransport.jsonTransport.with< + OpenAIResponsesBody, + OpenAIResponsesWebSocketMessage +>({ toMessage: webSocketMessage, encodeMessage: encodeWebSocketMessage, }) @@ -579,15 +946,9 @@ export const webSocketRoute = Route.make({ id: `${ADAPTER}-websocket`, provider: "openai", protocol, + endpoint, + auth, transport: webSocketTransport, - defaults: routeDefaults, }) -// ============================================================================= -// Model Helper -// ============================================================================= -export const model = route.model - -export const webSocketModel = webSocketRoute.model - export * as OpenAIResponses from "./openai-responses" diff --git a/packages/llm/src/protocols/shared.ts b/packages/llm/src/protocols/shared.ts index b8067bbe905..aa37c62e436 100644 --- a/packages/llm/src/protocols/shared.ts +++ b/packages/llm/src/protocols/shared.ts @@ -11,6 +11,7 @@ import { type MediaPart, type ToolResultPart, } from "../schema" +export { isRecord } from "../utils/record" export const Json = Schema.fromJsonString(Schema.Unknown) export const decodeJson = Schema.decodeUnknownSync(Json) @@ -19,13 +20,6 @@ export const JsonObject = Schema.Record(Schema.String, Schema.Unknown) export const optionalArray = (schema: S) => Schema.optional(Schema.Array(schema)) export const optionalNull = (schema: S) => Schema.optional(Schema.NullOr(schema)) -/** - * Plain-record narrowing. Excludes arrays so routes checking nested JSON - * Schema fragments don't accidentally treat a tuple as a key/value bag. - */ -export const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) - /** * Streaming tool-call accumulator. Adapters that build a tool call across * multiple `tool-input-delta` chunks store the partial JSON input string here @@ -86,7 +80,7 @@ export const subtractTokens = (total: number | undefined, subtrahend: number | u */ export const sumTokens = (...values: ReadonlyArray): number | undefined => { if (values.every((value) => value === undefined)) return undefined - return values.reduce((acc, value) => acc + (value ?? 0), 0) + return values.reduce((acc: number, value) => acc + (value ?? 0), 0) } export const eventError = (route: string, message: string, raw?: string) => @@ -128,10 +122,21 @@ export const parseToolInput = (route: string, name: string, raw: string) => export const mediaBytes = (part: MediaPart) => typeof part.data === "string" ? part.data : Buffer.from(part.data).toString("base64") +export const mediaBase64 = (part: MediaPart) => { + if (typeof part.data !== "string" || !part.data.startsWith("data:")) return mediaBytes(part) + return part.data.slice(part.data.indexOf(",") + 1) +} + +export const mediaDataUrl = (part: MediaPart) => + typeof part.data === "string" && part.data.startsWith("data:") + ? part.data + : `data:${part.mediaType};base64,${mediaBytes(part)}` + export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") export const toolResultText = (part: ToolResultPart) => { if (part.result.type === "text" || part.result.type === "error") return String(part.result.value) + if (part.result.type === "content") return encodeJson(part.result.value) return encodeJson(part.result.value) } diff --git a/packages/llm/src/protocols/utils/bedrock-auth.ts b/packages/llm/src/protocols/utils/bedrock-auth.ts index 58d16d95f81..37cc451256d 100644 --- a/packages/llm/src/protocols/utils/bedrock-auth.ts +++ b/packages/llm/src/protocols/utils/bedrock-auth.ts @@ -1,15 +1,14 @@ import { AwsV4Signer } from "aws4fetch" -import { Effect, Option, Schema } from "effect" +import { Effect } from "effect" import { Headers } from "effect/unstable/http" import { Auth, type AuthInput } from "../../route/auth" -import type { LLMRequest } from "../../schema" import { ProviderShared } from "../shared" /** - * AWS credentials for SigV4 signing. Bedrock also supports Bearer API key auth - * via `model.apiKey`, which bypasses SigV4 signing. STS-vended credentials - * should be refreshed by the consumer (rebuild the model) before they expire; - * the route does not refresh. + * AWS credentials for SigV4 signing. Bedrock also supports Bearer API key auth, + * which provider facades configure as route auth instead of SigV4. STS-vended + * credentials should be refreshed by the consumer (rebuild the model) before + * they expire; the route does not refresh. */ export interface Credentials { readonly region: string @@ -18,32 +17,6 @@ export interface Credentials { readonly sessionToken?: string } -const NativeCredentials = Schema.Struct({ - accessKeyId: Schema.String, - secretAccessKey: Schema.String, - region: Schema.optional(Schema.String), - sessionToken: Schema.optional(Schema.String), -}) - -const decodeNativeCredentials = Schema.decodeUnknownOption(NativeCredentials) - -export const region = (request: LLMRequest) => { - const fromNative = request.model.native?.aws_region - if (typeof fromNative === "string" && fromNative !== "") return fromNative - return ( - decodeNativeCredentials(request.model.native?.aws_credentials).pipe( - Option.map((credentials) => credentials.region), - Option.getOrUndefined, - ) ?? "us-east-1" - ) -} - -const credentialsFromInput = (request: LLMRequest): Credentials | undefined => - decodeNativeCredentials(request.model.native?.aws_credentials).pipe( - Option.map((creds) => ({ ...creds, region: creds.region ?? region(request) })), - Option.getOrUndefined, - ) - const signRequest = (input: { readonly url: string readonly body: string @@ -71,33 +44,27 @@ const signRequest = (input: { ), }) -/** - * Bedrock auth. `model.apiKey` (Bedrock's newer Bearer API key auth) wins if - * set; otherwise sign the exact JSON bytes with SigV4 using credentials from - * `model.native.aws_credentials`. - */ -export const auth = Auth.custom((input: AuthInput) => { - if (input.request.model.apiKey) return Auth.toEffect(Auth.bearer())(input) - return Effect.gen(function* () { - const credentials = credentialsFromInput(input.request) - if (!credentials) { - return yield* ProviderShared.invalidRequest( - "Bedrock Converse requires either model.apiKey or AWS credentials in model.native.aws_credentials", - ) - } - const headersForSigning = Headers.set(input.headers, "content-type", "application/json") - const signed = yield* signRequest({ url: input.url, body: input.body, headers: headersForSigning, credentials }) - return Headers.setAll(headersForSigning, signed) - }) -}) - -export const nativeCredentials = (native: Record | undefined, credentials: Credentials | undefined) => - credentials - ? { - ...native, - aws_credentials: credentials, - aws_region: credentials.region, +/** Sign the exact JSON bytes with SigV4 using credentials configured on the route. */ +export const sigV4 = (credentials: Credentials | undefined) => + Auth.custom((input: AuthInput) => { + return Effect.gen(function* () { + if (!credentials) { + return yield* ProviderShared.invalidRequest( + "Bedrock Converse requires either route bearer auth or AWS credentials configured on the route", + ) } - : native + const headersForSigning = Headers.set(input.headers, "content-type", "application/json") + const signed = yield* signRequest({ + url: input.url, + body: input.body, + headers: headersForSigning, + credentials, + }) + return Headers.setAll(headersForSigning, signed) + }) + }) + +/** Bedrock route auth defaults to SigV4 and expects credentials from route configuration. */ +export const auth = sigV4(undefined) export * as BedrockAuth from "./bedrock-auth" diff --git a/packages/llm/src/protocols/utils/lifecycle.ts b/packages/llm/src/protocols/utils/lifecycle.ts index c249d75cee8..21301df47ae 100644 --- a/packages/llm/src/protocols/utils/lifecycle.ts +++ b/packages/llm/src/protocols/utils/lifecycle.ts @@ -24,16 +24,24 @@ export const textDelta = (state: State, events: LLMEvent[], id: string, text: st return { ...stepped, text: new Set([...stepped.text, id]) } } -export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { +export const reasoningStart = ( + state: State, + events: LLMEvent[], + id: string, + providerMetadata?: ProviderMetadata, +): State => { + if (state.reasoning.has(id)) return state const stepped = stepStart(state, events) - if (stepped.reasoning.has(id)) { - events.push(LLMEvent.reasoningDelta({ id, text })) - return stepped - } - events.push(LLMEvent.reasoningStart({ id }), LLMEvent.reasoningDelta({ id, text })) + events.push(LLMEvent.reasoningStart({ id, providerMetadata })) return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) } } +export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { + const started = reasoningStart(state, events, id) + events.push(LLMEvent.reasoningDelta({ id, text })) + return started +} + export const reasoningEnd = ( state: State, events: LLMEvent[], diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/llm/src/protocols/utils/openai-options.ts index 080ef83f50b..53c834be88d 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/llm/src/protocols/utils/openai-options.ts @@ -7,12 +7,28 @@ export const OpenAIReasoningEfforts = ReasoningEfforts.filter( ) export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number] +// Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this +// in lockstep with `openai-node/src/resources/responses/responses.ts`. +export const OpenAIResponseIncludables = [ + "file_search_call.results", + "web_search_call.results", + "web_search_call.action.sources", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "code_interpreter_call.outputs", + "reasoning.encrypted_content", + "message.output_text.logprobs", +] as const +export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number] + const REASONING_EFFORTS = new Set(ReasoningEfforts) const OPENAI_REASONING_EFFORTS = new Set(OpenAIReasoningEfforts) const TEXT_VERBOSITY = new Set(["low", "medium", "high"]) +const INCLUDABLES = new Set(OpenAIResponseIncludables) export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts) export const OpenAITextVerbosity = TextVerbosity +export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables) const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort => typeof effort === "string" && REASONING_EFFORTS.has(effort) @@ -35,12 +51,20 @@ export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefine return isAnyReasoningEffort(value) ? value : undefined } -export const reasoningSummary = (request: LLMRequest): "auto" | undefined => { - return options(request)?.reasoningSummary === "auto" ? "auto" : undefined -} +export const reasoningSummary = (request: LLMRequest): "auto" | undefined => + options(request)?.reasoningSummary === "auto" ? "auto" : undefined -export const encryptedReasoning = (request: LLMRequest) => - options(request)?.includeEncryptedReasoning === true ? true : undefined +// Resolve the OpenAI Responses `include` field. Filters out unknown +// includable values defensively so a typo in upstream config drops the +// invalid entry instead of poisoning the wire body. An empty array (either +// passed directly or produced by filtering) is treated as "no include" and +// returns undefined so the request body omits the field entirely. +export const include = (request: LLMRequest): ReadonlyArray | undefined => { + const value = options(request)?.include + if (!Array.isArray(value)) return undefined + const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry)) + return filtered.length > 0 ? filtered : undefined +} export const promptCacheKey = (request: LLMRequest) => { const value = options(request)?.promptCacheKey @@ -52,4 +76,9 @@ export const textVerbosity = (request: LLMRequest) => { return isTextVerbosity(value) ? value : undefined } +export const instructions = (request: LLMRequest) => { + const value = options(request)?.instructions + return typeof value === "string" ? value : undefined +} + export * as OpenAIOptions from "./openai-options" diff --git a/packages/llm/src/provider.ts b/packages/llm/src/provider.ts index 8299b5865ca..7f69583418b 100644 --- a/packages/llm/src/provider.ts +++ b/packages/llm/src/provider.ts @@ -1,14 +1,20 @@ -import type { RouteModelInput } from "./route/client" -import type { ModelID, ModelRef, ProviderID } from "./schema" +import type { RouteDefaultsInput } from "./route/client" +import type { Model, ModelID, ProviderID } from "./schema" -export type ModelOptions = Omit +export type ModelOptions = RouteDefaultsInput +/** + * Advanced structural provider definition helper. Built-in providers should + * prefer explicit `configure(options).model(id)` facades so deployment config is + * chosen before model selection. The optional `apis` map remains for external + * structural providers that expose multiple route selectors behind one provider. + */ export type ModelFactory = ( id: string | ModelID, options?: Options, -) => ModelRef +) => Model -type AnyModelFactory = (...args: never[]) => ModelRef +type AnyModelFactory = (...args: never[]) => Model export interface Definition { readonly id: ProviderID @@ -18,8 +24,8 @@ export interface Definition { type DefinitionShape = { readonly id: ProviderID - readonly model: (...args: never[]) => ModelRef - readonly apis?: Record ModelRef> + readonly model: (...args: never[]) => Model + readonly apis?: Record Model> } type NoExtraFields = Input & Record, never> diff --git a/packages/llm/src/providers/amazon-bedrock.ts b/packages/llm/src/providers/amazon-bedrock.ts index 82408d514e9..2f1791e0d62 100644 --- a/packages/llm/src/providers/amazon-bedrock.ts +++ b/packages/llm/src/providers/amazon-bedrock.ts @@ -1,12 +1,12 @@ -import { Route, type RouteModelInput } from "../route/client" -import { Provider } from "../provider" +import type { RouteDefaultsInput } from "../route/client" +import { Auth } from "../route/auth" import { ProviderID, type ModelID } from "../schema" import * as BedrockConverse from "../protocols/bedrock-converse" import type { BedrockCredentials } from "../protocols/bedrock-converse" export const id = ProviderID.make("amazon-bedrock") -export type ModelOptions = Omit & { +export type Config = RouteDefaultsInput & { readonly apiKey?: string readonly headers?: Record readonly credentials?: BedrockCredentials @@ -15,34 +15,29 @@ export type ModelOptions = Omit & { /** Override the computed `https://bedrock-runtime..amazonaws.com` URL. */ readonly baseURL?: string } -type ModelInput = ModelOptions & Pick - export const routes = [BedrockConverse.route] const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com` -const converseModel = Route.model( - BedrockConverse.route, - { - provider: "amazon-bedrock", - }, - { - mapInput: (input) => { - const { credentials, region, baseURL, ...rest } = input - const resolvedRegion = region ?? credentials?.region ?? "us-east-1" - return { - ...rest, - baseURL: baseURL ?? bedrockBaseURL(resolvedRegion), - native: BedrockConverse.nativeCredentials(input.native, credentials), - } - }, - }, -) +const configuredRoute = (input: Config) => { + const { apiKey, credentials, region, baseURL, ...rest } = input + const resolvedRegion = region ?? credentials?.region ?? "us-east-1" + return BedrockConverse.route.with({ + ...rest, + provider: id, + endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) }, + auth: apiKey === undefined ? BedrockConverse.sigV4Auth(credentials) : Auth.bearer(apiKey), + }) +} -export const model = (modelID: string | ModelID, options: ModelOptions = {}) => - converseModel({ ...options, id: modelID }) +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} -export const provider = Provider.make({ - id, - model, -}) +export const provider = configure() +export const model = provider.model diff --git a/packages/llm/src/providers/anthropic.ts b/packages/llm/src/providers/anthropic.ts index cca12bf7c21..0c9640af5ea 100644 --- a/packages/llm/src/providers/anthropic.ts +++ b/packages/llm/src/providers/anthropic.ts @@ -1,5 +1,6 @@ -import type { RouteModelInput } from "../route/client" -import { Provider } from "../provider" +import type { RouteDefaultsInput } from "../route/client" +import { Auth } from "../route/auth" +import type { ProviderAuthOption } from "../route/auth-options" import { ProviderID, type ModelID } from "../schema" import * as AnthropicMessages from "../protocols/anthropic-messages" @@ -7,12 +8,28 @@ export const id = ProviderID.make("anthropic") export const routes = [AnthropicMessages.route] -export const model = ( - id: string | ModelID, - options: Omit & { readonly baseURL?: string } = {}, -) => AnthropicMessages.model({ ...options, id }) +export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } -export const provider = Provider.make({ - id, - model, -}) +const auth = (options: ProviderAuthOption<"optional">) => { + if ("auth" in options && options.auth) return options.auth + return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") + .orElse(Auth.config("ANTHROPIC_API_KEY")) + .pipe(Auth.header("x-api-key")) +} + +const configuredRoute = (input: Config) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return AnthropicMessages.route.with({ ...rest, endpoint: { baseURL }, auth: auth(input) }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = configure() +export const model = provider.model diff --git a/packages/llm/src/providers/azure.ts b/packages/llm/src/providers/azure.ts index 8d60fb6669b..bfac2d1cad3 100644 --- a/packages/llm/src/providers/azure.ts +++ b/packages/llm/src/providers/azure.ts @@ -1,83 +1,110 @@ import { Auth } from "../route/auth" import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options" -import { Route } from "../route/client" -import type { ModelInput } from "../llm" -import { Provider } from "../provider" +import type { Route as RouteDef, RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options" export const id = ProviderID.make("azure") -const routeAuth = Auth.remove("authorization").andThen(Auth.apiKeyHeader("api-key")) +const routeAuth = Auth.remove("authorization") // Azure needs the customer's resource URL; supply either `resourceName` // (helper builds the URL) or `baseURL` directly. type AzureURL = AtLeastOne<{ readonly resourceName: string; readonly baseURL: string }> export type ModelOptions = AzureURL & - Omit & + RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly apiVersion?: string + readonly queryParams?: Record readonly useCompletionUrls?: boolean readonly providerOptions?: OpenAIProviderOptionsInput } -type AzureModelInput = ModelOptions & Pick +export type Config = ModelOptions const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai/v1` const responsesRoute = OpenAIResponses.route.with({ id: "azure-openai-responses", provider: id, - transport: OpenAIResponses.httpTransport.with({ auth: routeAuth }), + auth: routeAuth, + endpoint: { + query: { "api-version": "v1" }, + }, }) const chatRoute = OpenAIChat.route.with({ id: "azure-openai-chat", provider: id, - transport: OpenAIChat.httpTransport.with({ auth: routeAuth }), + auth: routeAuth, + endpoint: { + query: { "api-version": "v1" }, + }, }) export const routes = [responsesRoute, chatRoute] -const mapInput = (input: AzureModelInput) => { - const { apiKey: _, apiVersion, resourceName, useCompletionUrls, ...rest } = input - return { - ...withOpenAIOptions(input.id, rest), - auth: - "auth" in input && input.auth - ? input.auth - : Auth.remove("authorization").andThen( - Auth.optional("apiKey" in input ? input.apiKey : undefined, "apiKey") - .orElse(Auth.config("AZURE_OPENAI_API_KEY")) - .pipe(Auth.header("api-key")), - ), - // AtLeastOne guarantees at least one is set; baseURL wins if both are. - baseURL: rest.baseURL ?? resourceBaseURL(resourceName!), - queryParams: { - ...rest.queryParams, - "api-version": apiVersion ?? rest.queryParams?.["api-version"] ?? "v1", +const defaults = (input: Config) => { + const { + apiKey: _, + apiVersion: _apiVersion, + resourceName: _resourceName, + useCompletionUrls: _useCompletionUrls, + baseURL: _baseURL, + queryParams: _queryParams, + ...rest + } = input + if ("auth" in rest) { + const { auth: _, ...withoutAuth } = rest + return withoutAuth + } + return rest +} + +const auth = (input: Config) => { + if ("auth" in input && input.auth) return input.auth + return Auth.remove("authorization").andThen( + Auth.optional("apiKey" in input ? input.apiKey : undefined, "apiKey") + .orElse(Auth.config("AZURE_OPENAI_API_KEY")) + .pipe(Auth.header("api-key")), + ) +} + +const configuredRoute = (route: RouteDef, input: Config) => + route.with({ + auth: auth(input), + endpoint: { + // AtLeastOne guarantees at least one is set; baseURL wins if both are. + baseURL: input.baseURL ?? resourceBaseURL(input.resourceName!), + query: { + ...(input.apiVersion ? { "api-version": input.apiVersion } : {}), + ...input.queryParams, + }, }, + }) + +export const configure = (input: Config) => { + const configuredResponsesRoute = configuredRoute(responsesRoute, input) + const configuredChatRoute = configuredRoute(chatRoute, input) + const modelDefaults = defaults(input) + + const responses = (modelID: string | ModelID) => + configuredResponsesRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID }) + + const chat = (modelID: string | ModelID) => + configuredChatRoute.with(withOpenAIOptions(modelID, modelDefaults)).model({ id: modelID }) + + return { + id, + model: (modelID: string | ModelID) => (input.useCompletionUrls === true ? chat(modelID) : responses(modelID)), + responses, + chat, + configure, } } -const chatModel = Route.model(chatRoute, {}, { mapInput }) -const responsesModel = Route.model(responsesRoute, {}, { mapInput }) - -export const responses = (modelID: string | ModelID, options: ModelOptions) => - responsesModel({ ...options, id: modelID }) - -export const chat = (modelID: string | ModelID, options: ModelOptions) => chatModel({ ...options, id: modelID }) - -export const model = (modelID: string | ModelID, options: ModelOptions) => { - if (options.useCompletionUrls === true) return chat(modelID, options) - return responses(modelID, options) -} - -export const provider = Provider.make({ +export const provider = { id, - model, - apis: { responses, chat }, -}) - -export const apis = provider.apis + configure, +} diff --git a/packages/llm/src/providers/cloudflare.ts b/packages/llm/src/providers/cloudflare.ts index 263595a7550..a006152e982 100644 --- a/packages/llm/src/providers/cloudflare.ts +++ b/packages/llm/src/providers/cloudflare.ts @@ -1,19 +1,16 @@ import type { Config, Redacted } from "effect" -import { type ModelInput } from "../llm" -import { Provider } from "../provider" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" import { Auth } from "../route/auth" import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options" -import { Route } from "../route/client" +import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" export const aiGatewayID = ProviderID.make("cloudflare-ai-gateway") export const workersAIID = ProviderID.make("cloudflare-workers-ai") -export const id = aiGatewayID export const aiGatewayAuthEnvVars = ["CLOUDFLARE_API_TOKEN", "CF_AIG_TOKEN"] as const export const workersAIAuthEnvVars = ["CLOUDFLARE_API_KEY", "CLOUDFLARE_WORKERS_AI_TOKEN"] as const -type CloudflareSecret = string | Redacted.Redacted | Config.Config> +type CloudflareSecret = string | Redacted.Redacted | Config.Config type GatewayURL = AtLeastOne<{ readonly accountId: string @@ -23,32 +20,26 @@ type GatewayURL = AtLeastOne<{ } export type AIGatewayOptions = GatewayURL & - Omit & + RouteDefaultsInput & ProviderAuthOption<"optional"> & { /** Cloudflare AI Gateway authentication token. Sent as `cf-aig-authorization`. */ readonly gatewayApiKey?: CloudflareSecret } -type AIGatewayInput = AIGatewayOptions & Pick - type WorkersAIURL = AtLeastOne<{ readonly accountId: string readonly baseURL: string }> -export type WorkersAIOptions = WorkersAIURL & - Omit & - ProviderAuthOption<"optional"> - -type WorkersAIInput = WorkersAIOptions & Pick +export type WorkersAIOptions = WorkersAIURL & RouteDefaultsInput & ProviderAuthOption<"optional"> export const aiGatewayBaseURL = (input: GatewayURL) => { if (input.baseURL) return input.baseURL - if (!input.accountId) throw new Error("Cloudflare.aiGateway requires accountId unless baseURL is supplied") + if (!input.accountId) throw new Error("CloudflareAIGateway.configure requires accountId unless baseURL is supplied") return `https://gateway.ai.cloudflare.com/v1/${encodeURIComponent(input.accountId)}/${encodeURIComponent(input.gatewayId?.trim() || "default")}/compat` } -const aiGatewayAuth = (input: AIGatewayInput) => { +const aiGatewayAuth = (input: AIGatewayOptions) => { if ("auth" in input && input.auth) return input.auth const gateway = Auth.optional(input.gatewayApiKey, "gatewayApiKey") .orElse(Auth.config("CLOUDFLARE_API_TOKEN")) @@ -61,11 +52,11 @@ const aiGatewayAuth = (input: AIGatewayInput) => { export const workersAIBaseURL = (input: WorkersAIURL) => { if (input.baseURL) return input.baseURL - if (!input.accountId) throw new Error("Cloudflare.workersAI requires accountId unless baseURL is supplied") + if (!input.accountId) throw new Error("CloudflareWorkersAI.configure requires accountId unless baseURL is supplied") return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(input.accountId)}/ai/v1` } -const workersAIAuth = (input: WorkersAIInput) => { +const workersAIAuth = (input: WorkersAIOptions) => { return AuthOptions.bearer(input, workersAIAuthEnvVars) } @@ -81,59 +72,56 @@ export const workersAIRoute = OpenAICompatibleChat.route.with({ export const routes = [aiGatewayRoute, workersAIRoute] -const aiGatewayModel = Route.model( - aiGatewayRoute, - { - provider: id, - }, - { - mapInput: (input) => { - const { - accountId: _accountId, - gatewayId: _gatewayId, - apiKey: _apiKey, - gatewayApiKey: _gatewayApiKey, - auth: _auth, - ...rest - } = input - return { - ...rest, - auth: aiGatewayAuth(input), - baseURL: aiGatewayBaseURL(input), - } - }, - }, -) +const aiGatewayDefaults = (options: AIGatewayOptions) => { + const { + accountId: _accountId, + gatewayId: _gatewayId, + apiKey: _apiKey, + gatewayApiKey: _gatewayApiKey, + baseURL: _baseURL, + auth: _auth, + ...rest + } = options + return rest +} -const workersAIModel = Route.model( - workersAIRoute, - { - provider: workersAIID, - }, - { - mapInput: (input) => { - const { accountId: _accountId, apiKey: _apiKey, auth: _auth, ...rest } = input - return { - ...rest, - auth: workersAIAuth(input), - baseURL: workersAIBaseURL(input), - } - }, - }, -) +const workersAIDefaults = (options: WorkersAIOptions) => { + const { accountId: _accountId, apiKey: _apiKey, auth: _auth, baseURL: _baseURL, ...rest } = options + return rest +} -export const aiGateway = (modelID: string | ModelID, options: AIGatewayOptions) => - aiGatewayModel({ ...options, id: modelID }) +const configureAIGateway = (options: AIGatewayOptions) => { + const route = aiGatewayRoute.with({ + ...aiGatewayDefaults(options), + endpoint: { baseURL: aiGatewayBaseURL(options) }, + auth: aiGatewayAuth(options), + }) + return { + id: aiGatewayID, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure: configureAIGateway, + } +} -export const workersAI = (modelID: string | ModelID, options: WorkersAIOptions) => - workersAIModel({ ...options, id: modelID }) +const configureWorkersAI = (options: WorkersAIOptions) => { + const route = workersAIRoute.with({ + ...workersAIDefaults(options), + endpoint: { baseURL: workersAIBaseURL(options) }, + auth: workersAIAuth(options), + }) + return { + id: workersAIID, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure: configureWorkersAI, + } +} -export const model = aiGateway +export const CloudflareAIGateway = { + id: aiGatewayID, + configure: configureAIGateway, +} -export const provider = Provider.make({ - id, - model, - apis: { aiGateway, workersAI }, -}) - -export const apis = provider.apis +export const CloudflareWorkersAI = { + id: workersAIID, + configure: configureWorkersAI, +} diff --git a/packages/llm/src/providers/github-copilot.ts b/packages/llm/src/providers/github-copilot.ts index 5de738a3bfc..fa6bba74229 100644 --- a/packages/llm/src/providers/github-copilot.ts +++ b/packages/llm/src/providers/github-copilot.ts @@ -1,6 +1,5 @@ -import { Route } from "../route/client" -import type { ModelInput } from "../llm" -import { Provider } from "../provider" +import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" +import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" @@ -10,10 +9,11 @@ export const id = ProviderID.make("github-copilot") // GitHub Copilot has no canonical public URL — callers (opencode, etc.) must // supply `baseURL` explicitly. -export type ModelOptions = Omit & { - readonly providerOptions?: OpenAIProviderOptionsInput -} -type CopilotModelInput = ModelOptions & Pick +export type ModelOptions = Omit & + ProviderAuthOption<"optional"> & { + readonly baseURL: string + readonly providerOptions?: OpenAIProviderOptionsInput + } export const shouldUseResponsesApi = (modelID: string | ModelID) => { const model = String(modelID) @@ -24,25 +24,43 @@ export const shouldUseResponsesApi = (modelID: string | ModelID) => { export const routes = [OpenAIResponses.route, OpenAIChat.route] -const mapInput = (input: CopilotModelInput) => withOpenAIOptions(input.id, input) +const chatRoute = OpenAIChat.route.with({ provider: id }) +const responsesRoute = OpenAIResponses.route.with({ provider: id }) -const chatModel = Route.model(OpenAIChat.route, { provider: id }, { mapInput }) -const responsesModel = Route.model(OpenAIResponses.route, { provider: id }, { mapInput }) - -export const responses = (modelID: string | ModelID, options: ModelOptions) => - responsesModel({ ...options, id: modelID }) - -export const chat = (modelID: string | ModelID, options: ModelOptions) => chatModel({ ...options, id: modelID }) - -export const model = (modelID: string | ModelID, options: ModelOptions) => { - const create = shouldUseResponsesApi(modelID) ? responsesModel : chatModel - return create({ ...options, id: modelID }) +const defaults = (options: ModelOptions) => { + const { apiKey: _, auth: _auth, baseURL: _baseURL, ...rest } = options + return rest } -export const provider = Provider.make({ - id, - model, - apis: { responses, chat }, -}) +const configuredResponsesRoute = (options: ModelOptions) => + responsesRoute.with({ + endpoint: { baseURL: options.baseURL }, + auth: AuthOptions.bearer(options, []), + }) -export const apis = provider.apis +const configuredChatRoute = (options: ModelOptions) => + chatRoute.with({ + endpoint: { baseURL: options.baseURL }, + auth: AuthOptions.bearer(options, []), + }) + +export const configure = (options: ModelOptions) => { + const responsesRoute = configuredResponsesRoute(options) + const chatRoute = configuredChatRoute(options) + const responses = (modelID: string | ModelID) => + responsesRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID }) + const chat = (modelID: string | ModelID) => + chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model({ id: modelID }) + return { + id, + model: (modelID: string | ModelID) => (shouldUseResponsesApi(modelID) ? responses(modelID) : chat(modelID)), + responses, + chat, + configure, + } +} + +export const provider = { + id, + configure, +} diff --git a/packages/llm/src/providers/google.ts b/packages/llm/src/providers/google.ts index c03b9a7c25d..c8a72c31f67 100644 --- a/packages/llm/src/providers/google.ts +++ b/packages/llm/src/providers/google.ts @@ -1,5 +1,6 @@ -import type { RouteModelInput } from "../route/client" -import { Provider } from "../provider" +import type { RouteDefaultsInput } from "../route/client" +import { Auth } from "../route/auth" +import type { ProviderAuthOption } from "../route/auth-options" import { ProviderID, type ModelID } from "../schema" import * as Gemini from "../protocols/gemini" @@ -7,12 +8,28 @@ export const id = ProviderID.make("google") export const routes = [Gemini.route] -export const model = ( - id: string | ModelID, - options: Omit & { readonly baseURL?: string } = {}, -) => Gemini.model({ ...options, id }) +export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } -export const provider = Provider.make({ - id, - model, -}) +const auth = (options: ProviderAuthOption<"optional">) => { + if ("auth" in options && options.auth) return options.auth + return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") + .orElse(Auth.config("GOOGLE_GENERATIVE_AI_API_KEY")) + .pipe(Auth.header("x-goog-api-key")) +} + +const configuredRoute = (input: Config) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return Gemini.route.with({ ...rest, endpoint: { baseURL }, auth: auth(input) }) +} + +export const configure = (input: Config = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} + +export const provider = configure() +export const model = provider.model diff --git a/packages/llm/src/providers/index.ts b/packages/llm/src/providers/index.ts index 39adbe25c0c..774274cf2d4 100644 --- a/packages/llm/src/providers/index.ts +++ b/packages/llm/src/providers/index.ts @@ -2,6 +2,7 @@ export * as Anthropic from "./anthropic" export * as AmazonBedrock from "./amazon-bedrock" export * as Azure from "./azure" export * as Cloudflare from "./cloudflare" +export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare" export * as GitHubCopilot from "./github-copilot" export * as Google from "./google" export * as OpenAI from "./openai" diff --git a/packages/llm/src/providers/openai-compatible.ts b/packages/llm/src/providers/openai-compatible.ts index e37dcb4adff..a79f65f6dfe 100644 --- a/packages/llm/src/providers/openai-compatible.ts +++ b/packages/llm/src/providers/openai-compatible.ts @@ -1,56 +1,60 @@ -import { Provider } from "../provider" import { ProviderID, type ModelID } from "../schema" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" -import type { OpenAICompatibleChatModelInput } from "../protocols/openai-compatible-chat" +import type { RouteDefaultsInput } from "../route/client" +import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile" export const id = ProviderID.make("openai-compatible") -export type ModelOptions = Omit & { - readonly provider: string -} +type GenericModelOptions = RouteDefaultsInput & + ProviderAuthOption<"optional"> & { + readonly provider?: string + readonly baseURL: string + } -type GenericModelOptions = Omit & { - readonly provider?: string -} - -export type FamilyModelOptions = Omit & { - readonly baseURL?: string -} +export type FamilyModelOptions = RouteDefaultsInput & + ProviderAuthOption<"optional"> & { + readonly baseURL?: string + } export const routes = [OpenAICompatibleChat.route] -export const model = (id: string | ModelID, options: ModelOptions) => { - return OpenAICompatibleChat.model({ - ...options, - id, - provider: ProviderID.make(options.provider), +export const configure = (input: GenericModelOptions) => { + const provider = input.provider ?? "openai-compatible" + const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input + const route = OpenAICompatibleChat.route.with({ + ...rest, + provider, + endpoint: { baseURL }, + auth: AuthOptions.bearer(input, []), }) + return { + id: ProviderID.make(provider), + model: (modelID: string | ModelID) => route.model({ id: modelID, provider: ProviderID.make(provider) }), + configure, + } } -export const profileModel = ( - profile: OpenAICompatibleProfile, - id: string | ModelID, - options: FamilyModelOptions = {}, -) => - OpenAICompatibleChat.model({ - ...options, - id, - provider: profile.provider, - baseURL: options.baseURL ?? profile.baseURL, - }) +const define = (profile: OpenAICompatibleProfile) => { + const configureProfile = (input: FamilyModelOptions = {}) => { + const facade = configure({ + ...input, + baseURL: input.baseURL ?? profile.baseURL, + provider: profile.provider, + }) + return { + id: ProviderID.make(profile.provider), + model: facade.model, + configure: configureProfile, + } + } + return configureProfile() +} -const define = (profile: OpenAICompatibleProfile) => - Provider.make({ - id: ProviderID.make(profile.provider), - model: (id: string | ModelID, options: FamilyModelOptions = {}) => profileModel(profile, id, options), - }) - -export const provider = Provider.make({ +export const provider = { id, - model: (id: string | ModelID, options: GenericModelOptions) => - model(id, { ...options, provider: options.provider ?? "openai-compatible" }), -}) + configure, +} export const baseten = define(profiles.baseten) export const cerebras = define(profiles.cerebras) diff --git a/packages/llm/src/providers/openai-options.ts b/packages/llm/src/providers/openai-options.ts index 8d3980f6099..329db08d786 100644 --- a/packages/llm/src/providers/openai-options.ts +++ b/packages/llm/src/providers/openai-options.ts @@ -1,5 +1,8 @@ import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema" import { mergeProviderOptions } from "../schema" +import type { OpenAIResponseIncludable } from "../protocols/utils/openai-options" + +export type { OpenAIResponseIncludable } from "../protocols/utils/openai-options" export interface OpenAIOptionsInput { readonly [key: string]: unknown @@ -7,7 +10,10 @@ export interface OpenAIOptionsInput { readonly promptCacheKey?: string readonly reasoningEffort?: ReasoningEffort readonly reasoningSummary?: "auto" - readonly includeEncryptedReasoning?: boolean + // OpenAI Responses `include` wire field. Mirrors the official SDK's + // `ResponseIncludable[]` union exactly so AI SDK callers and direct + // native-SDK callers share one shape and no translation is required. + readonly include?: ReadonlyArray readonly textVerbosity?: TextVerbosity } @@ -25,7 +31,7 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide promptCacheKey: options?.promptCacheKey, reasoningEffort: options?.reasoningEffort, reasoningSummary: options?.reasoningSummary, - includeEncryptedReasoning: options?.includeEncryptedReasoning, + include: options?.include, textVerbosity: options?.textVerbosity, }), ) @@ -42,6 +48,12 @@ export const gpt5DefaultOptions = ( return openAIProviderOptions({ reasoningEffort: "medium", reasoningSummary: "auto", + // GPT-5 reasoning models are configured stateless (`store: false`) by + // `openAIDefaultOptions` below, so the only way a follow-up turn can + // carry reasoning state is via the encrypted reasoning include. Without + // this, callers using the default model facade get reasoning summaries + // they cannot replay statelessly. + include: ["reasoning.encrypted_content"], textVerbosity: options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat") ? "low" @@ -59,10 +71,9 @@ export const withOpenAIOptions = { +): Omit & { readonly providerOptions?: ProviderOptions } => { return { ...options, - id: modelID, providerOptions: mergeProviderOptions(openAIDefaultOptions(modelID, defaults), options.providerOptions), } } diff --git a/packages/llm/src/providers/openai.ts b/packages/llm/src/providers/openai.ts index cbd9b995229..098cad84939 100644 --- a/packages/llm/src/providers/openai.ts +++ b/packages/llm/src/providers/openai.ts @@ -1,12 +1,11 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" -import type { RouteModelInput } from "../route/client" -import { Provider } from "../provider" +import type { Route, RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIResponses from "../protocols/openai-responses" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options" -export type { OpenAIOptionsInput } from "./openai-options" +export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options" export const id = ProviderID.make("openai") @@ -15,39 +14,50 @@ export const routes = [OpenAIResponses.route, OpenAIResponses.webSocketRoute, Op // This provider facade wraps the lower-level Responses and Chat model factories // with OpenAI-specific conveniences: typed options, API-key sugar, env fallback, // and default option normalization. -type OpenAIModelInput = Omit & +export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string + readonly queryParams?: Record readonly providerOptions?: OpenAIProviderOptionsInput } const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY") -export const responses = (id: string | ModelID, options: OpenAIModelInput> = {}) => { - const { apiKey: _, ...rest } = options - return OpenAIResponses.model(withOpenAIOptions(id, { ...rest, auth: auth(options) }, { textVerbosity: true })) +const defaults = (input: Config) => { + const { apiKey: _, auth: _auth, baseURL: _baseURL, queryParams: _queryParams, ...rest } = input + return rest } -export const responsesWebSocket = ( - id: string | ModelID, - options: OpenAIModelInput> = {}, -) => { - const { apiKey: _, ...rest } = options - return OpenAIResponses.webSocketModel( - withOpenAIOptions(id, { ...rest, auth: auth(options) }, { textVerbosity: true }), - ) +const configuredRoute = (route: Route, input: Config) => + route.with({ + auth: auth(input), + endpoint: { baseURL: input.baseURL, query: input.queryParams }, + }) + +export const configure = (input: Config = {}) => { + const responsesRoute = configuredRoute(OpenAIResponses.route, input) + const responsesWebSocketRoute = configuredRoute(OpenAIResponses.webSocketRoute, input) + const chatRoute = configuredRoute(OpenAIChat.route, input) + const modelDefaults = defaults(input) + const responses = (id: string | ModelID) => + responsesRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id }) + const responsesWebSocket = (id: string | ModelID) => + responsesWebSocketRoute.with(withOpenAIOptions(id, modelDefaults, { textVerbosity: true })).model({ id }) + const chat = (id: string | ModelID) => chatRoute.with(withOpenAIOptions(id, modelDefaults)).model({ id }) + + return { + id, + model: responses, + responses, + responsesWebSocket, + chat, + configure, + } } -export const chat = (id: string | ModelID, options: OpenAIModelInput> = {}) => { - const { apiKey: _, ...rest } = options - return OpenAIChat.model(withOpenAIOptions(id, { ...rest, auth: auth(options) })) -} - -export const provider = Provider.make({ - id, - model: responses, - apis: { responses, responsesWebSocket, chat }, -}) +export const provider = configure() export const model = provider.model -export const apis = provider.apis +export const responses = provider.responses +export const responsesWebSocket = provider.responsesWebSocket +export const chat = provider.chat diff --git a/packages/llm/src/providers/openrouter.ts b/packages/llm/src/providers/openrouter.ts index 4c1a4321061..914d7c0a0ba 100644 --- a/packages/llm/src/providers/openrouter.ts +++ b/packages/llm/src/providers/openrouter.ts @@ -1,9 +1,9 @@ import { Effect, Schema } from "effect" -import { Route, type RouteModelInput } from "../route/client" +import { Route, type RouteDefaultsInput } from "../route/client" import { Endpoint } from "../route/endpoint" import { Framing } from "../route/framing" -import { Provider } from "../provider" import { Protocol } from "../route/protocol" +import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import { ProviderID, type ModelID, type ProviderOptions } from "../schema" import * as OpenAICompatibleProfiles from "./openai-compatible-profile" import * as OpenAIChat from "../protocols/openai-chat" @@ -24,11 +24,11 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & { readonly openrouter?: OpenRouterOptions } -export type ModelOptions = Omit & { - readonly baseURL?: string - readonly providerOptions?: OpenRouterProviderOptionsInput -} -type ModelInput = ModelOptions & Pick +export type ModelOptions = Omit & + ProviderAuthOption<"optional"> & { + readonly baseURL?: string + readonly providerOptions?: OpenRouterProviderOptionsInput + } const OpenRouterBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields), [ Schema.Record(Schema.String, Schema.Any), @@ -68,21 +68,31 @@ const bodyOptions = (input: unknown) => { export const route = Route.make({ id: ADAPTER, + provider: profile.provider, protocol, - endpoint: Endpoint.path("/chat/completions"), + endpoint: Endpoint.path("/chat/completions", { baseURL: profile.baseURL }), framing: Framing.sse, }) export const routes = [route] -const modelRef = Route.model(route, { - provider: profile.provider, - baseURL: profile.baseURL, -}) +const configuredRoute = (input: ModelOptions) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return route.with({ + ...rest, + endpoint: { baseURL: baseURL ?? profile.baseURL }, + auth: AuthOptions.bearer(input, "OPENROUTER_API_KEY"), + }) +} -export const model = (id: string | ModelID, options: ModelOptions = {}) => modelRef({ ...options, id }) +export const configure = (input: ModelOptions = {}) => { + const route = configuredRoute(input) + return { + id, + model: (modelID: string | ModelID) => route.model({ id: modelID }), + configure, + } +} -export const provider = Provider.make({ - id, - model, -}) +export const provider = configure() +export const model = provider.model diff --git a/packages/llm/src/providers/xai.ts b/packages/llm/src/providers/xai.ts index 089c8c7339a..321db97db10 100644 --- a/packages/llm/src/providers/xai.ts +++ b/packages/llm/src/providers/xai.ts @@ -1,7 +1,5 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" -import { Route } from "../route/client" -import type { RouteModelInput } from "../route/client" -import { Provider } from "../provider" +import type { RouteDefaultsInput } from "../route/client" import { ProviderID, type ModelID } from "../schema" import * as OpenAICompatibleProfiles from "./openai-compatible-profile" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" @@ -9,44 +7,50 @@ import * as OpenAIResponses from "../protocols/openai-responses" export const id = ProviderID.make("xai") -export type ModelOptions = Omit & +export type ModelOptions = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } export const routes = [OpenAIResponses.route, OpenAICompatibleChat.route] -const responsesModel = Route.model(OpenAIResponses.route, { provider: id }) -const chatModel = OpenAICompatibleChat.model - const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "XAI_API_KEY") -export const responses = (modelID: string | ModelID, options: ModelOptions = {}) => { - const { apiKey: _, ...rest } = options - return responsesModel({ +const configuredResponsesRoute = (input: ModelOptions) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return OpenAIResponses.route.with({ ...rest, - auth: auth(options), - id: modelID, - baseURL: options.baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL, - }) -} - -export const chat = (modelID: string | ModelID, options: ModelOptions = {}) => { - const { apiKey: _, ...rest } = options - return chatModel({ - ...rest, - auth: auth(options), - id: modelID, provider: id, - baseURL: options.baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL, + endpoint: { baseURL: baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL }, + auth: auth(input), }) } -export const provider = Provider.make({ - id, - model: responses, - apis: { responses, chat }, -}) +const configuredChatRoute = (input: ModelOptions) => { + const { apiKey: _, auth: _auth, baseURL, ...rest } = input + return OpenAICompatibleChat.route.with({ + ...rest, + provider: id, + endpoint: { baseURL: baseURL ?? OpenAICompatibleProfiles.profiles.xai.baseURL }, + auth: auth(input), + }) +} +export const configure = (input: ModelOptions = {}) => { + const responsesRoute = configuredResponsesRoute(input) + const chatRoute = configuredChatRoute(input) + const responses = (modelID: string | ModelID) => responsesRoute.model({ id: modelID }) + const chat = (modelID: string | ModelID) => chatRoute.model({ id: modelID }) + return { + id, + model: responses, + responses, + chat, + configure, + } +} + +export const provider = configure() export const model = provider.model -export const apis = provider.apis +export const responses = provider.responses +export const chat = provider.chat diff --git a/packages/llm/src/route/auth.ts b/packages/llm/src/route/auth.ts index b46e2233634..32871c04547 100644 --- a/packages/llm/src/route/auth.ts +++ b/packages/llm/src/route/auth.ts @@ -12,6 +12,7 @@ export class MissingCredentialError extends Error { export type CredentialError = MissingCredentialError | Config.ConfigError export type AuthError = CredentialError | LLMError +type Secret = string | Redacted.Redacted | Config.Config export interface AuthInput { readonly request: LLMRequest @@ -22,7 +23,7 @@ export interface AuthInput { } export interface Credential { - readonly load: Effect.Effect, CredentialError> + readonly load: Effect.Effect readonly orElse: (that: Credential) => Credential readonly bearer: () => Auth readonly header: (name: string) => Auth @@ -39,7 +40,7 @@ export interface Auth { export const isAuth = (input: unknown): input is Auth => typeof input === "object" && input !== null && "apply" in input && typeof input.apply === "function" -const credential = (load: Effect.Effect, CredentialError>): Credential => { +const credential = (load: Effect.Effect): Credential => { const self: Credential = { load, orElse: (that) => credential(load.pipe(Effect.catch(() => that.load))), @@ -66,16 +67,13 @@ const fromCredential = (source: Credential, render: (secret: string) => Headers. source.load.pipe(Effect.map((secret) => Headers.setAll(input.headers, render(Redacted.value(secret))))), ) -const secretEffect = (secret: string | Redacted.Redacted, source: string) => { +const secretEffect = (secret: string | Redacted.Redacted, source: string) => { const redacted = typeof secret === "string" ? Redacted.make(secret) : secret if (Redacted.value(redacted) === "") return Effect.fail(new MissingCredentialError(source)) return Effect.succeed(redacted) } -const credentialFromSecret = ( - secret: string | Redacted.Redacted | Config.Config>, - source: string, -) => { +const credentialFromSecret = (secret: Secret, source: string) => { if (typeof secret === "string" || Redacted.isRedacted(secret)) return credential(secretEffect(secret, source)) return credential( Effect.gen(function* () { @@ -86,17 +84,14 @@ const credentialFromSecret = ( export const value = (secret: string, source = "value") => credentialFromSecret(secret, source) -export const optional = ( - secret: string | Redacted.Redacted | Config.Config> | undefined, - source = "optional value", -) => +export const optional = (secret: Secret | undefined, source = "optional value") => secret === undefined ? credential(Effect.fail(new MissingCredentialError(source))) : credentialFromSecret(secret, source) export const config = (name: string) => credentialFromSecret(Config.redacted(name), name) -export const effect = (load: Effect.Effect, CredentialError>) => credential(load) +export const effect = (load: Effect.Effect) => credential(load) export const none = auth((input) => Effect.succeed(input.headers)) @@ -109,68 +104,32 @@ export const custom = (apply: (input: AuthInput) => Effect.Effect Headers.Input) => - auth(({ request, headers }) => { - const key = request.model.apiKey - if (!key) return Effect.succeed(headers) - return Effect.succeed(Headers.setAll(headers, from(key))) - }) - -const credentialInput = ( - source: string | Redacted.Redacted | Config.Config> | Credential, -) => +const credentialInput = (source: Secret | Credential) => typeof source === "string" || Redacted.isRedacted(source) || Config.isConfig(source) ? credentialFromSecret(source, "value") : source -export function bearer(): Auth -export function bearer( - source: string | Redacted.Redacted | Config.Config> | Credential, -): Auth -export function bearer( - source?: string | Redacted.Redacted | Config.Config> | Credential, -) { - if (source === undefined) return fromModelApiKey((key) => ({ authorization: `Bearer ${key}` })) +export function bearer(source: Secret | Credential): Auth +export function bearer(source: Secret | Credential) { return credentialInput(source).bearer() } export const apiKey = bearer -export const apiKeyHeader = (name: string) => fromModelApiKey((key) => ({ [name]: key })) - -export function header( - name: string, -): (source: string | Redacted.Redacted | Config.Config> | Credential) => Auth -export function header( - name: string, - source: string | Redacted.Redacted | Config.Config> | Credential, -): Auth -export function header( - name: string, - source?: string | Redacted.Redacted | Config.Config> | Credential, -) { +export function header(name: string): (source: Secret | Credential) => Auth +export function header(name: string, source: Secret | Credential): Auth +export function header(name: string, source?: Secret | Credential) { if (source === undefined) { - return ( - next: string | Redacted.Redacted | Config.Config> | Credential, - ) => credentialInput(next).header(name) + return (next: Secret | Credential) => credentialInput(next).header(name) } return credentialInput(source).header(name) } -export function bearerHeader( - name: string, -): (source: string | Redacted.Redacted | Config.Config> | Credential) => Auth -export function bearerHeader( - name: string, - source: string | Redacted.Redacted | Config.Config> | Credential, -): Auth -export function bearerHeader( - name: string, - source?: string | Redacted.Redacted | Config.Config> | Credential, -) { - const render = ( - input: string | Redacted.Redacted | Config.Config> | Credential, - ) => fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` })) +export function bearerHeader(name: string): (source: Secret | Credential) => Auth +export function bearerHeader(name: string, source: Secret | Credential): Auth +export function bearerHeader(name: string, source?: Secret | Credential) { + const render = (input: Secret | Credential) => + fromCredential(credentialInput(input), (secret) => ({ [name]: `Bearer ${secret}` })) if (source === undefined) return render return render(source) } diff --git a/packages/llm/src/route/client.ts b/packages/llm/src/route/client.ts index 2d9de2fd39d..63993bc2b71 100644 --- a/packages/llm/src/route/client.ts +++ b/packages/llm/src/route/client.ts @@ -1,31 +1,28 @@ import { Cause, Context, Effect, Layer, Schema, Stream } from "effect" -import type { Auth as AuthDef } from "./auth" -import type { Endpoint } from "./endpoint" +import * as Option from "effect/Option" +import { Auth, type Auth as AuthDef } from "./auth" +import { Endpoint, type EndpointPatch } from "./endpoint" import { RequestExecutor } from "./executor" import type { Framing } from "./framing" import { HttpTransport } from "./transport" import type { Transport, TransportRuntime } from "./transport" import { WebSocketExecutor } from "./transport" -import type { Service as WebSocketExecutorService } from "./transport/websocket" import type { Protocol } from "./protocol" import { applyCachePolicy } from "../cache-policy" import * as ProviderShared from "../protocols/shared" import * as ToolRuntime from "../tool-runtime" import type { Tools } from "../tool" -import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID } from "../schema" +import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" import { GenerationOptions, HttpOptions, LLMRequest, LLMResponse, - ModelID, + Model, ModelLimits, - ModelRef, LLMError as LLMErrorClass, - NoRouteReason, PreparedRequest, ProviderID, - RouteID, mergeGenerationOptions, mergeHttpOptions, mergeProviderOptions, @@ -42,11 +39,13 @@ export interface Route { readonly id: string readonly provider?: ProviderID readonly protocol: ProtocolID + readonly endpoint: Endpoint + readonly auth: AuthDef readonly transport: Transport readonly defaults: RouteDefaults readonly body: RouteBody readonly with: (patch: RoutePatch) => Route - readonly model: (input: Input) => ModelRef + readonly model: (input: RouteMappedModelInput) => Model readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect readonly streamPrepared: ( prepared: Prepared, @@ -61,116 +60,77 @@ export interface Route { // oxlint-disable-next-line typescript-eslint/no-explicit-any export type AnyRoute = Route -const routeRegistry = new Map() - -// Route lookup is intentionally global: model refs name a route id, and -// importing the provider/protocol/custom-route module registers the runnable -// implementation. Duplicate ids are bugs because model refs cannot disambiguate -// them. -const register = (route: R): R => { - const existing = routeRegistry.get(route.id) - if (existing && existing !== route) throw new Error(`Duplicate LLM route id "${route.id}"`) - routeRegistry.set(route.id, route) - return route -} - -const registeredRoute = (id: string) => routeRegistry.get(id) - export type HttpOptionsInput = HttpOptions.Input -export type ModelRefInput = Omit< - ConstructorParameters[0], - "id" | "provider" | "route" | "limits" | "generation" | "http" | "auth" -> & { - readonly id: string | ModelID - readonly provider: string | ProviderID - readonly route: string | RouteID - readonly auth?: AuthDef +export type RouteModelInput = Omit + +export type RouteRoutedModelInput = Omit + +export interface RouteDefaults { + readonly headers?: Record + readonly limits?: ModelLimits + readonly generation?: GenerationOptions + readonly providerOptions?: ProviderOptions + readonly http?: HttpOptions +} + +export interface RouteDefaultsInput { + readonly headers?: Record readonly limits?: ModelLimits.Input readonly generation?: GenerationOptions.Input - readonly http?: HttpOptionsInput + readonly providerOptions?: ProviderOptions + readonly http?: HttpOptions.Input } -// `baseURL` is required on `ModelRefInput` (every materialized `ModelRef` has -// a host) but optional at the route-input layers below. The route's `defaults` -// can supply a canonical URL (e.g. OpenAI/Anthropic) so the user's input may -// omit it. Routes without a canonical URL (OpenAI-compatible, GitHub Copilot) -// re-tighten this in their own input type. -export type RouteModelInput = Omit & { - readonly baseURL?: string -} - -export type RouteModelDefaults = Omit & { - readonly baseURL?: string -} - -export type RouteRoutedModelInput = Omit & { - readonly baseURL?: string -} - -export type RouteRoutedModelDefaults = Partial> - -export type RouteDefaults = Partial> - -export interface RoutePatch extends RouteDefaults { - readonly id: string +export interface RoutePatch extends RouteDefaultsInput { + readonly id?: string readonly provider?: string | ProviderID + readonly auth?: AuthDef readonly transport?: Transport + readonly endpoint?: EndpointPatch } type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput -export interface RouteModelOptions< - Input extends RouteMappedModelInput, - Output extends RouteMappedModelInput = RouteMappedModelInput, -> { - readonly mapInput?: (input: Input) => Output +const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => { + const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined) + if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`) + if (!endpointBaseURL(route.endpoint)) + throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`) + return Model.make({ + ...mapped, + provider, + route, + }) } -export interface RouteMappedModelOptions { - readonly mapInput: (input: Input) => Output -} - -const modelWithDefaults = - ( - route: AnyRoute, - defaults: Partial>, - options: { readonly mapInput?: (input: Input) => RouteMappedModelInput }, - ) => - (input: Input) => { - const mapped = options.mapInput === undefined ? (input as RouteMappedModelInput) : options.mapInput(input) - const provider = defaults.provider ?? route.provider ?? ("provider" in mapped ? mapped.provider : undefined) - if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`) - const baseURL = mapped.baseURL ?? defaults.baseURL ?? route.defaults.baseURL - if (!baseURL) - throw new Error(`Route.model(${route.id}) requires a baseURL — supply it via input, defaults, or route defaults`) - const generation = mergeGenerationOptions(route.defaults.generation, defaults.generation) - const providerOptions = mergeProviderOptions(route.defaults.providerOptions, defaults.providerOptions) - const http = mergeHttpOptions(httpOptions(route.defaults.http), httpOptions(defaults.http)) - return modelRef({ - ...route.defaults, - ...defaults, - ...mapped, - baseURL, - provider, - route: route.id, - limits: mapped.limits ?? defaults.limits ?? route.defaults.limits, - generation: mergeGenerationOptions(generation, mapped.generation), - providerOptions: mergeProviderOptions(providerOptions, mapped.providerOptions), - http: mergeHttpOptions(http, httpOptions(mapped.http)), - }) +const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefaultsInput): RouteDefaults => { + const headers = mergeHeaders(base?.headers, patch.headers) + return { + ...base, + ...patch, + headers, + limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits), + generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)), + providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions), + http: mergeHttpOptions( + base?.http, + httpOptions(patch.http), + headers === undefined ? undefined : new HttpOptions({ headers }), + ), } +} -const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefaults): RouteDefaults => ({ - ...base, - ...patch, - limits: patch.limits ?? base?.limits, - generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)), - providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions), - http: mergeHttpOptions(httpOptions(base?.http), httpOptions(patch.http)), -}) +const endpointBaseURL = (endpoint: Endpoint) => + typeof endpoint.baseURL === "string" ? endpoint.baseURL : undefined -export const modelLimits = ModelLimits.make +const mergeHeaders = (...items: ReadonlyArray | undefined>) => { + const entries = items.flatMap((item) => + item === undefined ? [] : Object.entries(item).filter((entry): entry is [string, string] => entry[1] !== undefined), + ) + if (entries.length === 0) return undefined + return Object.fromEntries(entries) +} export const generationOptions = (input: GenerationOptions.Input | undefined) => input === undefined ? undefined : GenerationOptions.make(input) @@ -180,40 +140,6 @@ export const httpOptions = (input: HttpOptionsInput | undefined) => { return HttpOptions.make(input) } -export const modelRef = (input: ModelRefInput) => - new ModelRef({ - ...input, - id: ModelID.make(input.id), - provider: ProviderID.make(input.provider), - route: RouteID.make(input.route), - limits: modelLimits(input.limits), - generation: generationOptions(input.generation), - http: httpOptions(input.http), - }) - -function model( - route: AnyRoute, - defaults: RouteModelDefaults, - options?: RouteModelOptions, -): (input: Input) => ModelRef -function model( - route: AnyRoute, - defaults?: RouteRoutedModelDefaults, - options?: RouteModelOptions, -): (input: Input) => ModelRef -function model( - route: AnyRoute, - defaults: Partial>, - options: RouteMappedModelOptions, -): (input: Input) => ModelRef -function model( - route: AnyRoute, - defaults: Partial> = {}, - options: { readonly mapInput?: (input: Input) => RouteMappedModelInput } = {}, -) { - return modelWithDefaults(route, defaults, options) -} - export interface Interface { /** * Compile a request through protocol body construction, validation, and HTTP @@ -242,22 +168,16 @@ export interface GenerateMethod { export class Service extends Context.Service()("@opencode/LLMClient") {} -const noRoute = (model: ModelRef) => - new LLMErrorClass({ - module: "LLMClient", - method: "resolveRoute", - reason: new NoRouteReason({ route: model.route, provider: model.provider, model: model.id }), - }) - const resolveRequestOptions = (request: LLMRequest) => LLMRequest.update(request, { - generation: mergeGenerationOptions(request.model.generation, request.generation) ?? new GenerationOptions({}), - providerOptions: mergeProviderOptions(request.model.providerOptions, request.providerOptions), - http: mergeHttpOptions(request.model.http, request.http), + generation: + mergeGenerationOptions(request.model.route.defaults.generation, request.generation) ?? new GenerationOptions({}), + providerOptions: mergeProviderOptions(request.model.route.defaults.providerOptions, request.providerOptions), + http: mergeHttpOptions(request.model.route.defaults.http, request.http), }) export interface MakeInput { - /** Route id used in registry lookup and error messages. */ + /** Route id used in diagnostics and prepared request metadata. */ readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID @@ -265,27 +185,33 @@ export interface MakeInput { readonly protocol: Protocol /** Where the request is sent. */ readonly endpoint: Endpoint - /** Per-request transport auth. Model-level `Auth` overrides this. */ + /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ readonly auth?: AuthDef /** Stream framing — bytes -> frames before `protocol.stream.event` decoding. */ readonly framing: Framing /** Static / per-request headers added before `auth` runs. */ readonly headers?: (input: { readonly request: LLMRequest }) => Record - /** Model defaults used by the route's `.model(...)` helper. */ - readonly defaults?: RouteDefaults + /** Route/request defaults used when compiling requests for this route. */ + readonly defaults?: RouteDefaultsInput } export interface MakeTransportInput { - /** Route id used in registry lookup and error messages. */ + /** Route id used in diagnostics and prepared request metadata. */ readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID /** Semantic API contract — owns body construction, body schema, and parsing. */ readonly protocol: Protocol + /** Where the request is sent. */ + readonly endpoint: Endpoint + /** Per-request transport auth. Provider facades override this via `route.with(...)`. */ + readonly auth?: AuthDef + /** Static / per-request headers added before `auth` runs. */ + readonly headers?: (input: { readonly request: LLMRequest }) => Record /** Runnable transport route. */ readonly transport: Transport - /** Provider/model defaults used by the route's `.model(...)` helper. */ - readonly defaults?: RouteDefaults + /** Route/request defaults used when compiling requests for this route. */ + readonly defaults?: RouteDefaultsInput } const streamError = (route: string, message: string, cause: Cause.Cause) => { @@ -298,6 +224,7 @@ function makeFromTransport( input: MakeTransportInput, ): Route { const protocol = input.protocol + const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema)) const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event) const decodeEvent = (route: string) => (frame: Frame) => decodeEventEffect(frame).pipe( @@ -310,29 +237,44 @@ function makeFromTransport( ), ) - const build = (routeInput: MakeTransportInput): Route => { + type BuiltRouteInput = Omit, "defaults"> & { + readonly defaults?: RouteDefaults + } + + const build = (routeInput: BuiltRouteInput): Route => { const route: Route = { id: routeInput.id, provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider), protocol: protocol.id, + endpoint: routeInput.endpoint, + auth: routeInput.auth ?? Auth.none, transport: routeInput.transport, defaults: routeInput.defaults ?? {}, body: protocol.body, with: (patch: RoutePatch) => { - const { id, provider, transport, ...defaults } = patch - if (!id || id === routeInput.id) throw new Error(`Route.with(${routeInput.id}) requires a new route id`) + const { id, provider, auth, transport, endpoint, ...defaults } = patch return build({ ...routeInput, - id, + id: id ?? routeInput.id, provider: provider ?? routeInput.provider, + auth: auth ?? routeInput.auth, + endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint, transport: (transport as Transport | undefined) ?? routeInput.transport, - defaults: mergeRouteDefaults(routeInput.defaults, defaults), + defaults: mergeRouteDefaults(route.defaults, defaults), }) }, - model: (input: RouteModelInput): ModelRef => modelWithDefaults(route, {}, {})(input), - prepareTransport: routeInput.transport.prepare, + model: (input) => makeRouteModel(route, input), + prepareTransport: (body, request) => + routeInput.transport.prepare({ + body, + request, + endpoint: routeInput.endpoint, + auth: routeInput.auth ?? Auth.none, + encodeBody, + headers: routeInput.headers, + }), streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => { - const route = `${request.model.provider}/${request.model.route}` + const route = `${request.model.provider}/${request.model.route.id}` const events = routeInput.transport .frames(prepared, request, runtime) .pipe( @@ -341,7 +283,7 @@ function makeFromTransport( ) return events.pipe( Stream.mapAccumEffect( - protocol.stream.initial, + () => protocol.stream.initial(request), protocol.stream.step, protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined, ), @@ -349,10 +291,10 @@ function makeFromTransport( ) }, } satisfies Route - return register(route) + return route } - return build(input) + return build({ ...input, defaults: mergeRouteDefaults(undefined, input.defaults ?? {}) }) } export function make( @@ -381,18 +323,14 @@ export function make( ): Route | Route> { if ("transport" in input) return makeFromTransport(input) const protocol = input.protocol - const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema)) return makeFromTransport({ id: input.id, provider: input.provider, protocol, - transport: HttpTransport.httpJson({ - endpoint: input.endpoint, - auth: input.auth, - framing: input.framing, - encodeBody, - headers: input.headers, - }), + endpoint: input.endpoint, + auth: input.auth, + headers: input.headers, + transport: HttpTransport.httpJson({ framing: input.framing }), defaults: input.defaults, }) } @@ -402,8 +340,7 @@ export function make( // execute transport. const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) { const resolved = applyCachePolicy(resolveRequestOptions(request)) - const route = registeredRoute(resolved.model.route) - if (!route) return yield* noRoute(resolved.model) + const route = resolved.model.route const body = yield* route.body .from(resolved) @@ -495,31 +432,21 @@ export const streamRequest = (request: LLMRequest) => export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { - const stream = streamWith(streamRequestWith({ http: yield* RequestExecutor.Service })) + const stream = streamWith( + streamRequestWith({ + http: yield* RequestExecutor.Service, + webSocket: Option.getOrUndefined(yield* Effect.serviceOption(WebSocketExecutor.Service)), + }), + ) return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) }) }), ) -export const layerWithWebSocket: Layer.Layer = - Layer.effect( - Service, - Effect.gen(function* () { - const stream = streamWith( - streamRequestWith({ - http: yield* RequestExecutor.Service, - webSocket: yield* WebSocketExecutor.Service, - }), - ) - return Service.of({ prepare: prepareWith as Interface["prepare"], stream, generate: generateWith(stream) }) - }), - ) - -export const Route = { make, model } as const +export const Route = { make } as const export const LLMClient = { Service, layer, - layerWithWebSocket, prepare, stream, generate, diff --git a/packages/llm/src/route/endpoint.ts b/packages/llm/src/route/endpoint.ts index 361ad508e1c..accbe532431 100644 --- a/packages/llm/src/route/endpoint.ts +++ b/packages/llm/src/route/endpoint.ts @@ -11,28 +11,42 @@ export type EndpointPart = string | ((input: EndpointInput) => strin /** * Declarative URL construction for one route. * - * `Endpoint` carries only the path. The host always lives on `model.baseURL`, - * supplied by the provider helper that constructs the model. `render(...)` - * just appends the path (and any `model.queryParams`) to that host. + * `Endpoint` carries URL construction for one route. Routes with a canonical + * host put `baseURL` here; provider helpers can override it by configuring the + * route before selecting a model. * * `path` may be a string or a function of `EndpointInput`, for routes whose * URL embeds the model id, region, or another body field (e.g. Bedrock, * Gemini). */ export interface Endpoint { + readonly baseURL?: string readonly path: EndpointPart + readonly query?: Record } +export type EndpointPatch = Partial> + /** Construct an `Endpoint` from a path string or path function. */ -export const path = (value: EndpointPart): Endpoint => ({ path: value }) +export const path = (value: EndpointPart, options: Omit, "path"> = {}): Endpoint => ({ + ...options, + path: value, +}) + +export const merge = (base: Endpoint, patch: EndpointPatch): Endpoint => ({ + ...base, + ...patch, + baseURL: patch.baseURL ?? base.baseURL, + path: patch.path ?? base.path, + query: patch.query === undefined ? base.query : { ...base.query, ...patch.query }, +}) const renderPart = (part: EndpointPart, input: EndpointInput) => typeof part === "function" ? part(input) : part export const render = (endpoint: Endpoint, input: EndpointInput) => { - const url = new URL(`${ProviderShared.trimBaseUrl(input.request.model.baseURL)}${renderPart(endpoint.path, input)}`) - const params = input.request.model.queryParams - if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value) + const url = new URL(`${ProviderShared.trimBaseUrl(endpoint.baseURL ?? "")}${renderPart(endpoint.path, input)}`) + for (const [key, value] of Object.entries(endpoint.query ?? {})) url.searchParams.set(key, value) return url } diff --git a/packages/llm/src/route/index.ts b/packages/llm/src/route/index.ts index a75dd3e0387..48f4b7bc339 100644 --- a/packages/llm/src/route/index.ts +++ b/packages/llm/src/route/index.ts @@ -1,14 +1,13 @@ -export { Route, LLMClient, modelLimits, modelRef } from "./client" +export { Route, LLMClient } from "./client" export type { Route as RouteShape, - RouteModelDefaults, RouteModelInput, - RouteRoutedModelDefaults, RouteRoutedModelInput, + RouteDefaults, + RouteDefaultsInput, AnyRoute, Interface as LLMClientShape, Service as LLMClientService, - ModelRefInput, } from "./client" export * from "./executor" export { Auth } from "./auth" diff --git a/packages/llm/src/route/protocol.ts b/packages/llm/src/route/protocol.ts index 3ce0f7827dd..acb1e78c67b 100644 --- a/packages/llm/src/route/protocol.ts +++ b/packages/llm/src/route/protocol.ts @@ -52,8 +52,8 @@ export interface ProtocolBody { export interface ProtocolStream { /** Schema for one decoded streaming event, decoded from a transport frame. */ readonly event: Schema.Codec - /** Initial parser state. Called once per response. */ - readonly initial: () => State + /** Initial parser state. Called once per response with the resolved request. */ + readonly initial: (request: LLMRequest) => State /** Translate one event into emitted `LLMEvent`s plus the next state. */ readonly step: (state: State, event: Event) => Effect.Effect], LLMError> /** Optional request-completion signal for transports that do not end naturally. */ diff --git a/packages/llm/src/route/transport/http.ts b/packages/llm/src/route/transport/http.ts index 2159ce90b0f..00508957a76 100644 --- a/packages/llm/src/route/transport/http.ts +++ b/packages/llm/src/route/transport/http.ts @@ -1,20 +1,13 @@ import { Effect, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" -import { Auth, type Auth as AuthDef } from "../auth" -import { type Endpoint, render as renderEndpoint } from "../endpoint" -import type { Framing } from "../framing" -import type { Transport } from "./index" +import { Auth } from "../auth" +import { render as renderEndpoint } from "../endpoint" +import { Framing, type Framing as FramingDef } from "../framing" +import type { Transport, TransportPrepareInput } from "./index" import * as ProviderShared from "../../protocols/shared" import { mergeJsonRecords, type LLMRequest } from "../../schema" -export interface JsonRequestInput { - readonly body: Body - readonly request: LLMRequest - readonly endpoint: Endpoint - readonly auth: AuthDef - readonly encodeBody: (body: Body) => string - readonly headers?: (input: { readonly request: LLMRequest }) => Record -} +export type JsonRequestInput = TransportPrepareInput export interface JsonRequestParts { readonly url: string @@ -25,7 +18,7 @@ export interface JsonRequestParts { export interface HttpPrepared { readonly request: HttpClientRequest.HttpClientRequest - readonly framing: Framing + readonly framing: FramingDef } const applyQuery = (url: string, query: Record | undefined) => { @@ -52,28 +45,21 @@ export const jsonRequestParts = (input: JsonRequestInput) => input.request.http?.query, ) const body = yield* bodyWithOverlay(input.body, input.request, input.encodeBody) - const headers = yield* Auth.toEffect(Auth.isAuth(input.request.model.auth) ? input.request.model.auth : input.auth)( - { - request: input.request, - method: "POST", - url, - body: body.bodyText, - headers: Headers.fromInput({ - ...(input.headers?.({ request: input.request }) ?? {}), - ...input.request.model.headers, - ...input.request.http?.headers, - }), - }, - ) + const headers = yield* Auth.toEffect(input.auth)({ + request: input.request, + method: "POST", + url, + body: body.bodyText, + headers: Headers.fromInput({ + ...input.headers?.({ request: input.request }), + ...input.request.http?.headers, + }), + }) return { url, jsonBody: body.jsonBody, bodyText: body.bodyText, headers } }) -export interface HttpJsonInput { - readonly endpoint: Endpoint - readonly auth?: AuthDef - readonly framing: Framing - readonly encodeBody: (body: Body) => string - readonly headers?: (input: { readonly request: LLMRequest }) => Record +export interface HttpJsonInput<_Body, Frame> { + readonly framing: FramingDef } export type HttpJsonPatch = Partial> @@ -85,14 +71,9 @@ export interface HttpJsonTransport extends Transport(input: HttpJsonInput): HttpJsonTransport => ({ id: "http-json", with: (patch) => httpJson({ ...input, ...patch }), - prepare: (body, request) => + prepare: (prepareInput) => jsonRequestParts({ - body, - request, - endpoint: input.endpoint, - auth: input.auth ?? Auth.bearer(), - encodeBody: input.encodeBody, - headers: input.headers, + ...prepareInput, }).pipe( Effect.map((parts) => ({ request: ProviderShared.jsonPost({ url: parts.url, body: parts.bodyText, headers: parts.headers }), @@ -109,8 +90,8 @@ export const httpJson = (input: HttpJsonInput): HttpJs response.stream.pipe( Stream.mapError((error) => ProviderShared.eventError( - `${request.model.provider}/${request.model.route}`, - `Failed to read ${request.model.provider}/${request.model.route} stream`, + `${request.model.provider}/${request.model.route.id}`, + `Failed to read ${request.model.provider}/${request.model.route.id} stream`, ProviderShared.errorText(error), ), ), @@ -120,3 +101,8 @@ export const httpJson = (input: HttpJsonInput): HttpJs ), ), }) + +export const sseJson = { + id: "http-json/sse", + with: () => httpJson({ framing: Framing.sse }), +} as const diff --git a/packages/llm/src/route/transport/index.ts b/packages/llm/src/route/transport/index.ts index f4d5fb29b7f..fde9d6c4154 100644 --- a/packages/llm/src/route/transport/index.ts +++ b/packages/llm/src/route/transport/index.ts @@ -1,4 +1,6 @@ import type { Effect, Stream } from "effect" +import type { Endpoint } from "../endpoint" +import type { Auth } from "../auth" import type { Interface as RequestExecutorInterface } from "../executor" import type { Interface as WebSocketExecutorInterface } from "./websocket" import type { LLMError, LLMRequest } from "../../schema" @@ -10,7 +12,7 @@ export interface TransportRuntime { export interface Transport { readonly id: string - readonly prepare: (body: Body, request: LLMRequest) => Effect.Effect + readonly prepare: (input: TransportPrepareInput) => Effect.Effect readonly frames: ( prepared: Prepared, request: LLMRequest, @@ -18,5 +20,14 @@ export interface Transport { ) => Stream.Stream } +export interface TransportPrepareInput { + readonly body: Body + readonly request: LLMRequest + readonly endpoint: Endpoint + readonly auth: Auth + readonly encodeBody: (body: Body) => string + readonly headers?: (input: { readonly request: LLMRequest }) => Record +} + export * as HttpTransport from "./http" export { WebSocketExecutor, WebSocketTransport } from "./websocket" diff --git a/packages/llm/src/route/transport/websocket.ts b/packages/llm/src/route/transport/websocket.ts index 647a6db43dd..310121420c4 100644 --- a/packages/llm/src/route/transport/websocket.ts +++ b/packages/llm/src/route/transport/websocket.ts @@ -1,8 +1,6 @@ -import { Cause, Context, Effect, Queue, Stream } from "effect" +import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" import { Headers } from "effect/unstable/http" -import { Auth, type Auth as AuthDef } from "../auth" -import type { Endpoint } from "../endpoint" -import { LLMError, TransportReason, type LLMRequest } from "../../schema" +import { LLMError, TransportReason } from "../../schema" import * as HttpTransport from "./http" import type { Transport } from "./index" @@ -135,6 +133,8 @@ export const open = (input: WebSocketRequest) => }), }).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input))) +export const layer: Layer.Layer = Layer.succeed(Service, Service.of({ open })) + export const fromWebSocket = ( ws: globalThis.WebSocket, input: WebSocketRequest, @@ -213,12 +213,8 @@ export interface JsonPrepared { } export interface JsonInput { - readonly endpoint: Endpoint - readonly auth?: AuthDef - readonly encodeBody: (body: Body) => string readonly toMessage: (body: Body | Record) => Effect.Effect readonly encodeMessage: (message: Message) => string - readonly headers?: (input: { readonly request: LLMRequest }) => Record } export type JsonPatch = Partial> @@ -230,15 +226,10 @@ export interface JsonTransport extends Transport(input: JsonInput): JsonTransport => ({ id: "websocket-json", with: (patch) => json({ ...input, ...patch }), - prepare: (body, request) => + prepare: (prepareInput) => Effect.gen(function* () { const parts = yield* HttpTransport.jsonRequestParts({ - body, - request, - endpoint: input.endpoint, - auth: input.auth ?? Auth.bearer(), - encodeBody: input.encodeBody, - headers: input.headers, + ...prepareInput, }) return { url: yield* webSocketUrl(parts.url), @@ -270,8 +261,14 @@ export const json = (input: JsonInput): JsonTransp }, }) +export const jsonTransport = { + id: "websocket-json", + with: json, +} as const + export const WebSocketExecutor = { Service, + layer, open, fromWebSocket, messageText, @@ -279,4 +276,5 @@ export const WebSocketExecutor = { export const WebSocketTransport = { json, + jsonTransport, } as const diff --git a/packages/llm/src/schema/events.ts b/packages/llm/src/schema/events.ts index 63c9b7b7df5..dd3e6d03629 100644 --- a/packages/llm/src/schema/events.ts +++ b/packages/llm/src/schema/events.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids" -import { ModelRef } from "./options" +import { ModelSchema } from "./options" import { ToolResultValue } from "./messages" /** @@ -91,6 +91,7 @@ export const TextDelta = Schema.Struct({ type: Schema.tag("text-delta"), id: ContentBlockID, text: Schema.String, + providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.TextDelta" }) export type TextDelta = Schema.Schema.Type @@ -112,6 +113,7 @@ export const ReasoningDelta = Schema.Struct({ type: Schema.tag("reasoning-delta"), id: ContentBlockID, text: Schema.String, + providerMetadata: Schema.optional(ProviderMetadata), }).annotate({ identifier: "LLM.Event.ReasoningDelta" }) export type ReasoningDelta = Schema.Schema.Type @@ -288,7 +290,7 @@ export class PreparedRequest extends Schema.Class("LLM.Prepared id: Schema.String, route: RouteID, protocol: ProtocolID, - model: ModelRef, + model: ModelSchema, body: Schema.Unknown, metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} diff --git a/packages/llm/src/schema/messages.ts b/packages/llm/src/schema/messages.ts index c38a66d33d8..03b830e43c8 100644 --- a/packages/llm/src/schema/messages.ts +++ b/packages/llm/src/schema/messages.ts @@ -1,9 +1,7 @@ import { Schema } from "effect" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" -import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelRef, ProviderOptions } from "./options" - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) +import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" +import { isRecord } from "../utils/record" const systemPartSchema = Schema.Struct({ type: Schema.Literal("text"), @@ -41,20 +39,53 @@ export const MediaPart = Schema.Struct({ }).annotate({ identifier: "LLM.Content.Media" }) export type MediaPart = Schema.Schema.Type -const isToolResultValue = (value: unknown): value is ToolResultValue => - isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error") && "value" in value +export const ToolResultMediaPart = Schema.Struct({ + type: Schema.Literal("media"), + mediaType: Schema.String, + data: Schema.String, + filename: Schema.optional(Schema.String), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}).annotate({ identifier: "LLM.ToolResult.Media" }) +export type ToolResultMediaPart = Schema.Schema.Type -export const ToolResultValue = Object.assign( +export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart]) +export type ToolResultContentPart = Schema.Schema.Type + +// kilocode_change start - avoid circular inference rejected by Kilo's newer tsgo +const toolResultValueSchema = Schema.Union([ Schema.Struct({ - type: Schema.Literals(["json", "text", "error"]), + type: Schema.Literal("json"), value: Schema.Unknown, - }).annotate({ identifier: "LLM.ToolResult" }), - { - make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => - isToolResultValue(value) ? value : { type, value }, + }), + Schema.Struct({ + type: Schema.Literal("text"), + value: Schema.Unknown, + }), + Schema.Struct({ + type: Schema.Literal("error"), + value: Schema.Unknown, + }), + Schema.Struct({ + type: Schema.Literal("content"), + value: Schema.Array(ToolResultContentPart), + }), +]).annotate({ identifier: "LLM.ToolResult" }) +export type ToolResultValue = Schema.Schema.Type + +const isToolResultValue = (value: unknown): value is ToolResultValue => + isRecord(value) && + (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && + "value" in value + +export const ToolResultValue = Object.assign(toolResultValueSchema, { + is: isToolResultValue, + make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => { + if (isToolResultValue(value)) return value + if (type === "content") return { type, value: Array.isArray(value) ? value : [] } + return { type, value } }, -) -export type ToolResultValue = Schema.Schema.Type +}) +// kilocode_change end export const ToolCallPart = Object.assign( Schema.Struct({ @@ -197,7 +228,7 @@ export type ResponseFormat = Schema.Schema.Type export class LLMRequest extends Schema.Class("LLM.Request")({ id: Schema.optional(Schema.String), - model: ModelRef, + model: ModelSchema, system: Schema.Array(SystemPart), messages: Schema.Array(Message), tools: Schema.Array(ToolDefinition), diff --git a/packages/llm/src/schema/options.ts b/packages/llm/src/schema/options.ts index 0f40196f7d4..c02af6d1ed4 100644 --- a/packages/llm/src/schema/options.ts +++ b/packages/llm/src/schema/options.ts @@ -1,8 +1,7 @@ import { Schema } from "effect" -import { JsonSchema, ModelID, ProviderID, RouteID } from "./ids" - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) +import { JsonSchema, ModelID, ProviderID } from "./ids" +import type { AnyRoute } from "../route/client" +import { isRecord } from "../utils/record" export const mergeJsonRecords = ( ...items: ReadonlyArray | undefined> @@ -135,67 +134,59 @@ export namespace ModelLimits { input instanceof ModelLimits ? input : new ModelLimits(input ?? {}) } -export class ModelRef extends Schema.Class("LLM.ModelRef")({ - id: ModelID, - provider: ProviderID, - route: RouteID, - baseURL: Schema.String, - /** Provider-specific API key convenience. Provider helpers normalize this into `auth`. */ - apiKey: Schema.optional(Schema.String), - /** Optional transport auth policy. Opaque because it may contain functions. */ - auth: Schema.optional(Schema.Any), - headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), - /** - * Query params appended to the request URL by `Endpoint.baseURL`. Used for - * deployment-level URL-scoped settings such as Azure's `api-version` or any - * provider that requires a per-request key in the URL. Generic concern, so - * lives as a typed first-class field instead of `native`. - */ - queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)), - limits: ModelLimits, - /** Provider-neutral generation defaults. Request-level values override them. */ - generation: Schema.optional(GenerationOptions), - /** Provider-owned typed-at-the-facade options for non-portable knobs. */ - providerOptions: Schema.optional(ProviderOptions), - /** Serializable raw HTTP overlays applied to the final outgoing request. */ - http: Schema.optional(HttpOptions), - /** - * Provider-specific opaque options. Reach for this only when the value is - * genuinely provider-private and does not fit a typed axis (e.g. Bedrock's - * `aws_credentials` / `aws_region` for SigV4). Anything used by more than - * one route should grow into a typed field instead. - */ - native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), -}) {} +export class Model { + readonly id: ModelID + readonly provider: ProviderID + readonly route: AnyRoute -export namespace ModelRef { - export type Input = ConstructorParameters[0] + constructor(input: Model.ConstructorInput) { + this.id = input.id + this.provider = input.provider + this.route = input.route + } - export const input = (model: ModelRef): Input => ({ - id: model.id, - provider: model.provider, - route: model.route, - baseURL: model.baseURL, - apiKey: model.apiKey, - auth: model.auth, - headers: model.headers, - queryParams: model.queryParams, - limits: model.limits, - generation: model.generation, - providerOptions: model.providerOptions, - http: model.http, - native: model.native, - }) + static make(input: Model.Input) { + return new Model({ + id: ModelID.make(input.id), + provider: ProviderID.make(input.provider), + route: input.route, + }) + } - export const update = (model: ModelRef, patch: Partial) => { + static input(model: Model): Model.ConstructorInput { + return { + id: model.id, + provider: model.provider, + route: model.route, + } + } + + static update(model: Model, patch: Partial) { if (Object.keys(patch).length === 0) return model - return new ModelRef({ - ...input(model), + return Model.make({ + ...Model.input(model), ...patch, }) } } +export namespace Model { + export type ConstructorInput = { + readonly id: ModelID + readonly provider: ProviderID + readonly route: AnyRoute + } + + export type Input = Omit & { + readonly id: string | ModelID + readonly provider: string | ProviderID + } +} + +export type ModelInput = Model.Input + +export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" }) + export class CacheHint extends Schema.Class("LLM.CacheHint")({ type: Schema.Literals(["ephemeral", "persistent"]), ttlSeconds: Schema.optional(Schema.Number), diff --git a/packages/llm/src/tool-runtime.ts b/packages/llm/src/tool-runtime.ts index ef527faa21b..4f6bc834071 100644 --- a/packages/llm/src/tool-runtime.ts +++ b/packages/llm/src/tool-runtime.ts @@ -11,7 +11,8 @@ import { ToolCallPart, ToolFailure, ToolResultPart, - type ToolResultValue, + ToolResultValue, + type ToolResultValue as ToolResultValueType, Usage, } from "./schema" import { type AnyTool, type ExecutableTools, type Tools, toDefinitions } from "./tool" @@ -276,7 +277,10 @@ const appendStreamingText = ( state.assistantContent.push({ type, text, providerMetadata }) } -const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<{ result: ToolResultValue; error?: unknown }> => { +const dispatch = ( + tools: Tools, + call: ToolCallPart, +): Effect.Effect<{ result: ToolResultValueType; error?: unknown }> => { const tool = tools[call.name] if (!tool) return Effect.succeed({ result: { type: "error" as const, value: `Unknown tool: ${call.name}` } }) if (!tool.execute) @@ -285,7 +289,7 @@ const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<{ result: Too return decodeAndExecute(tool, call).pipe( Effect.catchTag("LLM.ToolFailure", (failure) => Effect.succeed({ - result: { type: "error" as const, value: failure.message } satisfies ToolResultValue, + result: { type: "error" as const, value: failure.message } satisfies ToolResultValueType, error: failure.error, }), ), @@ -293,7 +297,7 @@ const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<{ result: Too ) } -const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect => +const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect => tool._decode(call.input).pipe( Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), Effect.flatMap((decoded) => tool.execute!(decoded, { id: call.id, name: call.name })), @@ -307,10 +311,12 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect ({ type: "json", value: encoded })), + Effect.map( + (encoded): ToolResultValueType => (ToolResultValue.is(encoded) ? encoded : { type: "json", value: encoded }), + ), ) -const emitEvents = (call: ToolCallPart, result: ToolResultValue, error: unknown): ReadonlyArray => +const emitEvents = (call: ToolCallPart, result: ToolResultValueType, error: unknown): ReadonlyArray => result.type === "error" ? [ LLMEvent.toolError({ id: call.id, name: call.name, message: String(result.value), error }), @@ -321,7 +327,7 @@ const emitEvents = (call: ToolCallPart, result: ToolResultValue, error: unknown) const followUpRequest = ( request: LLMRequest, state: StepState, - dispatched: ReadonlyArray, + dispatched: ReadonlyArray, ) => LLMRequest.update(request, { messages: [ diff --git a/packages/llm/src/utils/record.ts b/packages/llm/src/utils/record.ts new file mode 100644 index 00000000000..a121fbde11a --- /dev/null +++ b/packages/llm/src/utils/record.ts @@ -0,0 +1,3 @@ +/** Plain-record narrowing. Excludes arrays so JSON object checks don't accept tuples as key/value bags. */ +export const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) diff --git a/packages/llm/test/adapter.test.ts b/packages/llm/test/adapter.test.ts index 80349a5ae5a..8e182948fa2 100644 --- a/packages/llm/test/adapter.test.ts +++ b/packages/llm/test/adapter.test.ts @@ -1,12 +1,12 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { LLM } from "../src" -import { Route, Endpoint, LLMClient, Protocol, type RouteModelInput, type FramingDef } from "../src/route" -import { ModelRef } from "../src/schema" +import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route" +import { Model } from "../src/schema" import { testEffect } from "./lib/effect" import { dynamicResponse } from "./lib/http" -const updateModel = (model: ModelRef, patch: Partial) => ModelRef.update(model, patch) +const updateModel = (model: Model, patch: Partial) => Model.update(model, patch) const Json = Schema.fromJsonString(Schema.Unknown) const encodeJson = Schema.encodeSync(Json) @@ -38,17 +38,6 @@ const fakeFraming: FramingDef = { ).pipe(Stream.flatMap(Stream.fromIterable)), } -const request = LLM.request({ - id: "req_1", - model: LLM.model({ - id: "fake-model", - provider: "fake-provider", - route: "fake", - baseURL: "https://fake.local", - }), - prompt: "hello", -}) - const raiseEvent = (event: FakeEvent): import("../src/schema").LLMEvent => event.type === "finish" ? { type: "finish", reason: event.reason } @@ -84,6 +73,7 @@ const fake = Route.make({ endpoint: Endpoint.path("/chat"), framing: fakeFraming, }) +const configuredFake = fake.with({ endpoint: { baseURL: "https://fake.local" } }) const gemini = Route.make({ id: "gemini-fake", @@ -91,6 +81,17 @@ const gemini = Route.make({ endpoint: Endpoint.path("/chat"), framing: fakeFraming, }) +const configuredGemini = gemini.with({ endpoint: { baseURL: "https://fake.local" } }) + +const request = LLM.request({ + id: "req_1", + model: Model.make({ + id: "fake-model", + provider: "fake-provider", + route: configuredFake, + }), + prompt: "hello", +}) const echoLayer = dynamicResponse(({ text, respond }) => Effect.succeed( @@ -117,61 +118,47 @@ describe("llm route", () => { }), ) - it.effect("selects routes by request route", () => + it.effect("selects routes by model route value", () => Effect.gen(function* () { const llm = yield* LLMClient.Service const prepared = yield* llm.prepare( - LLM.updateRequest(request, { model: updateModel(request.model, { route: "gemini-fake" }) }), + LLM.updateRequest(request, { model: updateModel(request.model, { route: configuredGemini }) }), ) expect(prepared.route).toBe("gemini-fake") }), ) - it.effect("maps model input before building refs", () => + it.effect("builds models from configured routes", () => Effect.gen(function* () { - const mapped = Route.model( - fake, - { provider: "fake-provider", baseURL: "https://fake.local" }, - { - mapInput: (input) => { - const { region, ...rest } = input - return { ...rest, native: { region } } + const configured = fake.with({ provider: "fake-provider", endpoint: { baseURL: "https://fake.local" } }) + + expect(configured.model({ id: "fake-model" })).toMatchObject({ + provider: "fake-provider", + }) + }), + ) + + it.effect("does not register duplicate route ids globally", () => + Effect.gen(function* () { + const duplicate = Route.make({ + id: "fake", + protocol: Protocol.make({ + ...fakeProtocol, + body: { + ...fakeProtocol.body, + from: () => Effect.succeed({ body: "late-default" }), }, - }, + }), + endpoint: Endpoint.path("/chat", { baseURL: "https://fake.local" }), + framing: fakeFraming, + }) + + const prepared = yield* (yield* LLMClient.Service).prepare( + LLM.updateRequest(request, { model: updateModel(request.model, { route: duplicate }) }), ) - expect(mapped({ id: "fake-model", region: "us-east-1" }).native).toEqual({ region: "us-east-1" }) - }), - ) - - it.effect("rejects duplicate route ids", () => - Effect.gen(function* () { - expect(() => - Route.make({ - id: "fake", - protocol: Protocol.make({ - ...fakeProtocol, - body: { - ...fakeProtocol.body, - from: () => Effect.succeed({ body: "late-default" }), - }, - }), - endpoint: Endpoint.path("/chat"), - framing: fakeFraming, - }), - ).toThrow('Duplicate LLM route id "fake"') - }), - ) - - it.effect("rejects missing route", () => - Effect.gen(function* () { - const llm = yield* LLMClient.Service - const error = yield* llm - .prepare(LLM.updateRequest(request, { model: updateModel(request.model, { route: "missing" }) })) - .pipe(Effect.flip) - - expect(error.message).toContain("No LLM route") + expect(prepared.body).toEqual({ body: "late-default" }) }), ) }) diff --git a/packages/llm/test/auth-options.types.ts b/packages/llm/test/auth-options.types.ts index a44efa2274f..18f9508c3ca 100644 --- a/packages/llm/test/auth-options.types.ts +++ b/packages/llm/test/auth-options.types.ts @@ -2,8 +2,17 @@ import { Config } from "effect" import type { Auth } from "../src/route/auth" import type { ModelFactory } from "../src/route/auth-options" import { Auth as RuntimeAuth } from "../src/route/auth" +import * as OpenAIChat from "../src/protocols/openai-chat" +import * as AmazonBedrock from "../src/providers/amazon-bedrock" +import * as Anthropic from "../src/providers/anthropic" import * as Azure from "../src/providers/azure" +import * as Cloudflare from "../src/providers/cloudflare" +import * as GitHubCopilot from "../src/providers/github-copilot" +import * as Google from "../src/providers/google" import * as OpenAI from "../src/providers/openai" +import * as OpenAICompatible from "../src/providers/openai-compatible" +import * as OpenRouter from "../src/providers/openrouter" +import * as XAI from "../src/providers/xai" type BaseOptions = { readonly baseURL?: string @@ -19,6 +28,20 @@ declare const optionalAuthModel: ModelFactory declare const requiredAuthModel: ModelFactory const configApiKey = Config.redacted("OPENAI_API_KEY") +OpenAIChat.route.model({ id: "gpt-4.1-mini" }) + +// @ts-expect-error route model selection does not configure endpoints. +OpenAIChat.route.model({ id: "gpt-4.1-mini", baseURL: "https://gateway.example.com/v1" }) + +// @ts-expect-error route model selection does not configure query params. +OpenAIChat.route.model({ id: "gpt-4.1-mini", queryParams: { debug: "1" } }) + +// @ts-expect-error route model selection does not configure auth. +OpenAIChat.route.model({ id: "gpt-4.1-mini", auth }) + +// @ts-expect-error route model selection does not configure api keys. +OpenAIChat.route.model({ id: "gpt-4.1-mini", apiKey: "sk-test" }) + optionalAuthModel("gpt-4.1-mini") optionalAuthModel("gpt-4.1-mini", {}) optionalAuthModel("gpt-4.1-mini", { apiKey: "sk-test" }) @@ -45,56 +68,101 @@ requiredAuthModel("custom-model", {}) requiredAuthModel("custom-model", { apiKey: "key", auth }) OpenAI.responses("gpt-4.1-mini") -OpenAI.responses("gpt-4.1-mini", {}) -OpenAI.responses("gpt-4.1-mini", { apiKey: "sk-test" }) -OpenAI.responses("gpt-4.1-mini", { apiKey: configApiKey }) -OpenAI.responses("gpt-4.1-mini", { auth: RuntimeAuth.bearer("oauth-token") }) -OpenAI.responses("gpt-4.1-mini", { +OpenAI.configure({}).responses("gpt-4.1-mini") +OpenAI.configure({ apiKey: "sk-test" }).responses("gpt-4.1-mini") +OpenAI.configure({ apiKey: configApiKey }).responses("gpt-4.1-mini") +OpenAI.configure({ auth: RuntimeAuth.bearer("oauth-token") }).responses("gpt-4.1-mini") +OpenAI.configure({ auth: RuntimeAuth.headers({ authorization: "Bearer gateway" }), baseURL: "https://gateway.example.com/v1", -}) -OpenAI.responses("gpt-4.1-mini", { +}).responses("gpt-4.1-mini") +OpenAI.configure({ generation: { maxTokens: 100 }, providerOptions: { openai: { store: false } }, -}) +}).responses("gpt-4.1-mini") + +// @ts-expect-error OpenAI model selectors only accept model ids. +OpenAI.configure({ apiKey: "sk-test" }).responses("gpt-4.1-mini", {}) // @ts-expect-error apiKey only accepts string, Redacted, or Config>. -OpenAI.responses("gpt-4.1-mini", { apiKey: 123 }) +OpenAI.configure({ apiKey: 123 }) // @ts-expect-error provider helpers reject unknown top-level options. -OpenAI.responses("gpt-4.1-mini", { bogus: true }) +OpenAI.configure({ bogus: true }) // @ts-expect-error common generation options remain typed. -OpenAI.responses("gpt-4.1-mini", { generation: { maxTokens: "many" } }) +OpenAI.configure({ generation: { maxTokens: "many" } }) // @ts-expect-error provider-native options remain typed. -OpenAI.responses("gpt-4.1-mini", { providerOptions: { openai: { store: "false" } } }) +OpenAI.configure({ providerOptions: { openai: { store: "false" } } }) // @ts-expect-error auth is an override, so OpenAI rejects apiKey with auth. -OpenAI.responses("gpt-4.1-mini", { apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") }) +OpenAI.configure({ apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") }) OpenAI.chat("gpt-4.1-mini") -OpenAI.chat("gpt-4.1-mini", { apiKey: "sk-test" }) -OpenAI.chat("gpt-4.1-mini", { apiKey: configApiKey }) -OpenAI.chat("gpt-4.1-mini", { auth: RuntimeAuth.bearer("oauth-token") }) +OpenAI.configure({ apiKey: "sk-test" }).chat("gpt-4.1-mini") +OpenAI.configure({ apiKey: configApiKey }).chat("gpt-4.1-mini") +OpenAI.configure({ auth: RuntimeAuth.bearer("oauth-token") }).chat("gpt-4.1-mini") + +// @ts-expect-error OpenAI chat selectors only accept model ids. +OpenAI.configure({ apiKey: "sk-test" }).chat("gpt-4.1-mini", {}) // @ts-expect-error auth is an override, so OpenAI Chat rejects apiKey with auth. -OpenAI.chat("gpt-4.1-mini", { apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") }) +OpenAI.configure({ apiKey: "sk-test", auth: RuntimeAuth.bearer("oauth-token") }) // @ts-expect-error Azure requires at least one of `resourceName` or `baseURL`. -Azure.responses("deployment") -Azure.responses("deployment", { apiKey: "azure-key", resourceName: "resource" }) -Azure.responses("deployment", { apiKey: configApiKey, resourceName: "resource" }) -Azure.responses("deployment", { auth: RuntimeAuth.header("api-key", "azure-key"), resourceName: "resource" }) +Azure.configure() +Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).responses("deployment") +Azure.configure({ apiKey: configApiKey, resourceName: "resource" }).responses("deployment") +Azure.configure({ auth: RuntimeAuth.header("api-key", "azure-key"), resourceName: "resource" }).responses("deployment") + +// @ts-expect-error Azure model selectors only accept deployment ids. +Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).responses("deployment", {}) // @ts-expect-error auth is an override, so Azure rejects apiKey with auth. -Azure.responses("deployment", { apiKey: "azure-key", auth: RuntimeAuth.header("api-key", "override") }) +Azure.configure({ resourceName: "resource", apiKey: "azure-key", auth: RuntimeAuth.header("api-key", "override") }) -// @ts-expect-error Azure requires at least one of `resourceName` or `baseURL`. -Azure.chat("deployment") -Azure.chat("deployment", { apiKey: "azure-key", resourceName: "resource" }) -Azure.chat("deployment", { apiKey: configApiKey, resourceName: "resource" }) -Azure.chat("deployment", { auth: RuntimeAuth.header("api-key", "azure-key"), resourceName: "resource" }) +Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).chat("deployment") +Azure.configure({ apiKey: configApiKey, resourceName: "resource" }).chat("deployment") +Azure.configure({ auth: RuntimeAuth.header("api-key", "azure-key"), resourceName: "resource" }).chat("deployment") + +// @ts-expect-error Azure chat model selectors only accept deployment ids. +Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).chat("deployment", {}) // @ts-expect-error auth is an override, so Azure Chat rejects apiKey with auth. -Azure.chat("deployment", { apiKey: "azure-key", auth: RuntimeAuth.header("api-key", "override") }) +Azure.configure({ resourceName: "resource", apiKey: "azure-key", auth: RuntimeAuth.header("api-key", "override") }) + +Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku") +// @ts-expect-error Anthropic model selectors only accept model ids. +Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku", {}) + +Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash") +// @ts-expect-error Google model selectors only accept model ids. +Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash", {}) + +AmazonBedrock.configure({ apiKey: "bedrock-key" }).model("anthropic.claude") +// @ts-expect-error Bedrock model selectors only accept model ids. +AmazonBedrock.configure({ apiKey: "bedrock-key" }).model("anthropic.claude", {}) + +OpenRouter.configure({ apiKey: "openrouter-key" }).model("openai/gpt-4o-mini") +// @ts-expect-error OpenRouter model selectors only accept model ids. +OpenRouter.configure({ apiKey: "openrouter-key" }).model("openai/gpt-4o-mini", {}) + +XAI.configure({ apiKey: "xai-key" }).responses("grok-4") +XAI.configure({ apiKey: "xai-key" }).chat("grok-4") +// @ts-expect-error xAI Responses selectors only accept model ids. +XAI.configure({ apiKey: "xai-key" }).responses("grok-4", {}) +// @ts-expect-error xAI Chat selectors only accept model ids. +XAI.configure({ apiKey: "xai-key" }).chat("grok-4", {}) + +OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat") +// @ts-expect-error OpenAI-compatible family selectors only accept model ids. +OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-chat", {}) + +Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama") +// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids. +Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama", {}) + +GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1") +// @ts-expect-error GitHub Copilot model selectors only accept model ids. +GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1", {}) diff --git a/packages/llm/test/auth.test.ts b/packages/llm/test/auth.test.ts index 6b53f4d5ebd..1c7148dbbb4 100644 --- a/packages/llm/test/auth.test.ts +++ b/packages/llm/test/auth.test.ts @@ -3,11 +3,13 @@ import { ConfigProvider, Effect } from "effect" import { Headers } from "effect/unstable/http" import { LLM } from "../src" import { Auth } from "../src/route/auth" +import * as OpenAIChat from "../src/protocols/openai-chat" +import { Model } from "../src/schema" import { it } from "./lib/effect" const request = LLM.request({ id: "req_auth", - model: LLM.model({ id: "fake-model", provider: "fake", route: "fake", baseURL: "https://fake.local" }), + model: Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }), prompt: "hello", }) diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index ac700b58fc7..a126d9502c5 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -1,36 +1,32 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { CacheHint, LLM, Message } from "../src" -import { LLMClient } from "../src/route" +import { Auth, LLMClient } from "../src/route" +import { AmazonBedrock } from "../src/providers" import * as AnthropicMessages from "../src/protocols/anthropic-messages" -import * as BedrockConverse from "../src/protocols/bedrock-converse" import * as Gemini from "../src/protocols/gemini" import * as OpenAIChat from "../src/protocols/openai-chat" import { applyCachePolicy } from "../src/cache-policy" import { it } from "./lib/effect" -const anthropicModel = AnthropicMessages.model({ - id: "claude-sonnet-4-5", - baseURL: "https://api.anthropic.test/v1/", - headers: { "x-api-key": "test" }, -}) +const anthropicModel = AnthropicMessages.route + .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) + .model({ id: "claude-sonnet-4-5" }) -const bedrockModel = BedrockConverse.model({ - id: "anthropic.claude-3-5-sonnet-20241022-v2:0", +const bedrockModel = AmazonBedrock.configure({ credentials: { region: "us-east-1", accessKeyId: "fixture", secretAccessKey: "fixture" }, -}) +}).model("anthropic.claude-3-5-sonnet-20241022-v2:0") -const openaiModel = OpenAIChat.model({ - id: "gpt-4o-mini", - baseURL: "https://api.openai.test/v1/", - headers: { authorization: "Bearer test" }, -}) +const openaiModel = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-4o-mini" }) -const geminiModel = Gemini.model({ - id: "gemini-2.5-flash", - baseURL: "https://generativelanguage.test/v1beta/", - headers: { "x-goog-api-key": "test" }, -}) +const geminiModel = Gemini.route + .with({ + endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, + auth: Auth.header("x-goog-api-key", "test"), + }) + .model({ id: "gemini-2.5-flash" }) describe("applyCachePolicy", () => { it.effect("undefined cache resolves to 'auto' (the recommended default)", () => diff --git a/packages/llm/test/continuation-scenarios.ts b/packages/llm/test/continuation-scenarios.ts new file mode 100644 index 00000000000..1bb1848b557 --- /dev/null +++ b/packages/llm/test/continuation-scenarios.ts @@ -0,0 +1,104 @@ +import { LLM, Message, ToolCallPart, ToolDefinition, ToolResultPart, type ContentPart, type Model } from "../src" + +export const basicContinuation = ["system", "user-text", "assistant-text", "user-follow-up"] as const +export const toolContinuation = ["tool-call", "tool-result"] as const +export const reasoningContinuation = ["assistant-reasoning", "encrypted-reasoning"] as const +export const mediaContinuation = ["user-image"] as const +export const maximalContinuation = [ + ...basicContinuation, + ...toolContinuation, + ...reasoningContinuation, + ...mediaContinuation, +] as const + +export type ContinuationFeature = (typeof maximalContinuation)[number] + +export const nativeOpenAIResponsesContinuation = [ + ...basicContinuation, + ...toolContinuation, + "encrypted-reasoning", + ...mediaContinuation, +] as const satisfies ReadonlyArray + +export const nativeAnthropicMessagesContinuation = [ + ...basicContinuation, + ...toolContinuation, + "assistant-reasoning", + ...mediaContinuation, +] as const satisfies ReadonlyArray + +export const continuationTool = ToolDefinition.make({ + name: "get_weather", + description: "Get current weather for a city.", + inputSchema: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + additionalProperties: false, + }, +}) + +export function continuationRequest(input: { + readonly id: string + readonly model: Model + readonly features: ReadonlyArray + readonly image?: string +}) { + const features = new Set(input.features) + const messages = [] + const firstUser: ContentPart[] = [] + const firstAssistant: ContentPart[] = [] + + if (features.has("user-text")) firstUser.push({ type: "text", text: "What is shown here?" }) + if (features.has("user-image")) + firstUser.push({ type: "media", mediaType: "image/png", data: input.image ?? "AAECAw==" }) + if (firstUser.length > 0) messages.push(Message.user(firstUser)) + + if (features.has("assistant-reasoning")) + firstAssistant.push({ + type: "reasoning", + text: "I inspected the previous turn.", + providerMetadata: { anthropic: { signature: "sig_continuation_1" } }, + }) + if (features.has("encrypted-reasoning")) + firstAssistant.push({ + type: "reasoning", + text: "I inspected the previous turn.", + providerMetadata: { + openai: { + itemId: "rs_continuation_1", + reasoningEncryptedContent: "encrypted-continuation-state", + }, + }, + }) + if (features.has("assistant-text")) firstAssistant.push({ type: "text", text: "It shows a small test image." }) + if (firstAssistant.length > 0) messages.push(Message.assistant(firstAssistant)) + + if (features.has("tool-call")) { + messages.push(Message.user("Check the weather in Paris before continuing.")) + messages.push( + Message.assistant([ToolCallPart.make({ id: "call_weather_1", name: "get_weather", input: { city: "Paris" } })]), + ) + } + if (features.has("tool-result")) { + messages.push( + Message.tool(ToolResultPart.make({ id: "call_weather_1", name: "get_weather", result: { temperature: 22 } })), + ) + if (features.has("assistant-text")) messages.push(Message.assistant("Paris is 22 degrees.")) + } + if (features.has("user-follow-up")) + messages.push(Message.user("Continue from this conversation in one short sentence.")) + + return LLM.request({ + id: input.id, + model: input.model, + system: features.has("system") ? "You are concise. Continue from the provided history." : undefined, + messages, + tools: features.has("tool-call") ? [continuationTool] : [], + cache: "none", + providerOptions: features.has("encrypted-reasoning") + ? { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } } + : undefined, + generation: { maxTokens: 80, temperature: 0 }, + }) +} diff --git a/packages/llm/test/endpoint.test.ts b/packages/llm/test/endpoint.test.ts index 43d2e1c5c43..504c9843c1b 100644 --- a/packages/llm/test/endpoint.test.ts +++ b/packages/llm/test/endpoint.test.ts @@ -1,37 +1,40 @@ import { describe, expect, test } from "bun:test" import { LLM } from "../src" +import * as OpenAIChat from "../src/protocols/openai-chat" import { Endpoint } from "../src/route" +import { Model } from "../src/schema" -const request = (input: { readonly baseURL: string; readonly queryParams?: Record }) => +const request = () => LLM.request({ - model: LLM.model({ + model: Model.make({ id: "model-1", provider: "test", - route: "test-route", - baseURL: input.baseURL, - queryParams: input.queryParams, + route: OpenAIChat.route, }), prompt: "hello", }) describe("Endpoint", () => { test("appends a static path to the model's baseURL", () => { - const url = Endpoint.render(Endpoint.path("/chat"), { - request: request({ baseURL: "https://api.example.test/v1/" }), + const url = Endpoint.render(Endpoint.path("/chat", { baseURL: "https://api.example.test/v1/" }), { + request: request(), body: {}, }) expect(url.toString()).toBe("https://api.example.test/v1/chat") }) - test("model query params are appended to the rendered URL", () => { - const url = Endpoint.render(Endpoint.path("/chat?alt=sse"), { - request: request({ + test("endpoint query params are appended to the rendered URL", () => { + const url = Endpoint.render( + Endpoint.path("/chat?alt=sse", { baseURL: "https://custom.example.test/root/", - queryParams: { "api-version": "2026-01-01", alt: "json" }, + query: { "api-version": "2026-01-01", alt: "json" }, }), - body: {}, - }) + { + request: request(), + body: {}, + }, + ) expect(url.toString()).toBe("https://custom.example.test/root/chat?alt=json&api-version=2026-01-01") }) @@ -40,9 +43,10 @@ describe("Endpoint", () => { const url = Endpoint.render( Endpoint.path<{ readonly modelId: string }>( ({ body }) => `/model/${encodeURIComponent(body.modelId)}/converse-stream`, + { baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com" }, ), { - request: request({ baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com" }), + request: request(), body: { modelId: "us.amazon.nova-micro-v1:0" }, }, ) diff --git a/packages/llm/test/executor.test.ts b/packages/llm/test/executor.test.ts index b294606ff34..fb8a4670787 100644 --- a/packages/llm/test/executor.test.ts +++ b/packages/llm/test/executor.test.ts @@ -106,8 +106,8 @@ describe("RequestExecutor", () => { expect(errorHttp(error)?.body).toBe("rate limited") }).pipe( Effect.provide( - responsesLayer([ - ...Array.from( + responsesLayer( + Array.from( { length: 3 }, () => new Response("rate limited", { @@ -115,7 +115,7 @@ describe("RequestExecutor", () => { headers: { "retry-after-ms": "0", "x-request-id": "req_123", "x-api-key": "secret" }, }), ), - ]), + ), ), ), ) @@ -388,7 +388,9 @@ describe("RequestExecutor", () => { it.effect("does not retry after a successful response reaches stream parsing", () => Effect.gen(function* () { const attempts = yield* Ref.make(0) - const model = OpenAIChat.model({ id: "gpt-4o-mini", baseURL: "https://api.openai.test/v1" }) + const model = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1" } }) + .model({ id: "gpt-4o-mini" }) const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe( Effect.provide( dynamicResponse((input) => diff --git a/packages/llm/test/exports.test.ts b/packages/llm/test/exports.test.ts index 237dadb27dc..693a21638ba 100644 --- a/packages/llm/test/exports.test.ts +++ b/packages/llm/test/exports.test.ts @@ -2,7 +2,14 @@ import { describe, expect, test } from "bun:test" import { LLM, LLMClient, Provider } from "@opencode-ai/llm" import { Route, Protocol } from "@opencode-ai/llm/route" import { Provider as ProviderSubpath } from "@opencode-ai/llm/provider" -import { Cloudflare, OpenAI, OpenAICompatible, OpenRouter, XAI } from "@opencode-ai/llm/providers" +import { + CloudflareAIGateway, + CloudflareWorkersAI, + OpenAI, + OpenAICompatible, + OpenRouter, + XAI, +} from "@opencode-ai/llm/providers" import * as GitHubCopilot from "@opencode-ai/llm/providers/github-copilot" import { OpenAIChat, OpenAICompatibleChat, OpenAIResponses } from "@opencode-ai/llm/protocols" import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages" @@ -24,26 +31,25 @@ describe("public exports", () => { test("provider barrels expose user-facing facades", () => { expect(OpenAI.model).toBeFunction() expect(OpenAI.provider.model).toBe(OpenAI.model) - expect(OpenAI.apis.responses).toBe(OpenAI.responses) - expect(OpenAI.apis.responsesWebSocket).toBe(OpenAI.responsesWebSocket) + expect(OpenAI.provider.responses).toBe(OpenAI.responses) + expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket) + expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction() expect(OpenAICompatible.deepseek.model).toBeFunction() - expect(Cloudflare.model).toBeFunction() - expect(Cloudflare.provider.model).toBe(Cloudflare.model) - expect(Cloudflare.aiGateway).toBeFunction() - expect(Cloudflare.workersAI).toBeFunction() + expect(CloudflareAIGateway.configure).toBeFunction() + expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction() + expect(CloudflareWorkersAI.configure).toBeFunction() + expect(CloudflareWorkersAI.configure({ accountId: "fixture", apiKey: "fixture" }).model).toBeFunction() expect(OpenRouter.model).toBeFunction() expect(OpenRouter.provider.model).toBe(OpenRouter.model) expect(XAI.model).toBeFunction() expect(XAI.provider.model).toBe(XAI.model) - expect(XAI.apis.responses).toBe(XAI.responses) - expect(XAI.apis.chat).toBe(XAI.chat) - expect(XAI.responses("grok-4.3", { apiKey: "fixture" })).toMatchObject({ - route: "openai-responses", - }) - expect(XAI.chat("grok-4.3", { apiKey: "fixture" })).toMatchObject({ - route: "openai-compatible-chat", - }) - expect(GitHubCopilot.model).toBeFunction() + expect(XAI.provider.responses).toBe(XAI.responses) + expect(XAI.provider.chat).toBe(XAI.chat) + expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses") + expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat") + expect( + GitHubCopilot.configure({ baseURL: "https://api.githubcopilot.test", apiKey: "fixture" }).model, + ).toBeFunction() }) test("protocol barrels expose supported low-level routes", () => { diff --git a/packages/llm/test/fixtures/media/restroom.png b/packages/llm/test/fixtures/media/restroom.png new file mode 100644 index 00000000000..52ed88afb0f Binary files /dev/null and b/packages/llm/test/fixtures/media/restroom.png differ diff --git a/packages/llm/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json b/packages/llm/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json new file mode 100644 index 00000000000..b1ba048d7a3 --- /dev/null +++ b/packages/llm/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "metadata": { + "name": "anthropic-messages/anthropic-opus-4-7-image-tool-result", + "recordedAt": "2026-05-22T01:57:05.693Z", + "provider": "anthropic", + "route": "anthropic-messages", + "transport": "http", + "model": "claude-opus-4-7", + "tags": [ + "prefix:anthropic-messages", + "provider:anthropic", + "flagship", + "media", + "image", + "vision", + "tool", + "tool-result", + "golden" + ] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.anthropic.com/v1/messages", + "headers": { + "anthropic-version": "2023-06-01", + "content-type": "application/json" + }, + "body": "{\"model\":\"claude-opus-4-7\",\"system\":[{\"type\":\"text\",\"text\":\"Read images carefully. Reply only with the visible text, lowercase, no punctuation.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use the read_screenshot tool, then reply with the words shown.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_screenshot_1\",\"name\":\"read_screenshot\",\"input\":{}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_screenshot_1\",\"content\":[{\"type\":\"text\",\"text\":\"Image read successfully\"},{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"media_type\":\"image/png\",\"data\":\"iVBORw0KGgoAAAANSUhEUgAAAnYAAACKCAYAAAAnmweyAAACKWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjYzMCIKICAgZXhpZjpVc2VyQ29tbWVudD0iU2NyZWVuc2hvdCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjEzOCIKICAgdGlmZjpZUmVzb2x1dGlvbj0iMTQ0LzEiCiAgIHRpZmY6WFJlc29sdXRpb249IjE0NC8xIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+at0SpgAACrhpQ0NQSUNDIFByb2ZpbGUAAEiJlZcHUFNZF8fvey+dhJYQASmh994CSAmhBVCQDjZCEiAQQkxBwa4sruBaUBHBsqKrIgo2qg0RxbYo9r4gi4iyLhZsqHwPGMLufvN933xn5s75zXnn/u+5d959cx4AFFOuRCKC1QHIFsul0SEBjMSkZAb+JcACTUACnoDK5ckkrKioCIDahP+7fbgLoFF/y25U69+f/1fT4AtkPACgKJRT+TJeNsonAIABTyKVA4CgDEwWyCWjfB9lmhQtEOWBUU4fY8yoDi11nGljObHRbJQtASCQuVxpOgBkVzTOyOWlozrkWJQdxXyhGOUClH2zs3P4KLehbInmSFAe1Wem/kUn/W+aqUpNLjddyeN7GTNCoFAmEXHz/s/j+N+WLVJMrGGBDnKGNDQa9Xrouf2elROuZHHqjMgJFvLH8sc4QxEaN8E8GTt5gmWiGM4E87mB4Uod0YyICU4TBitzhHJO7AQLZEExEyzNiVaumyZlsyaYK52sQZEVp4xnCDhK/fyM2IQJzhXGz1DWlhUTPpnDVsalimjlXgTikIDJdYOV55At+8vehRzlXHlGbKjyHLiT9QvErElNWaKyNr4gMGgyJ06ZL5EHKNeSiKKU+QJRiDIuy41RzpWjL+fk3CjlGWZyw6ImGMQAOVAAPhCCHMAAgaiXAQkQAS7IkwsWykc3xM6R5EmF6RlyBgu9dQIGR8yzt2U4Ozq7AzB6h8dfkXf0sbsJ0a9MxlZVAeDTNDIycnIyFnYDgKMpAJDqJmOWcwBQ7wPg0imeQpo7Hhu7a1j0y6AGaEAHGAATYAnsgDNwB97AHwSBMBAJYkESmAt4IANkAylYABaDFaAQFIMNYAsoB7vAHnAAHAbHQAM4Bc6Bi+AquAHugEegC/SCV2AQfADDEAThIQpEhXQgQ8gMsoGcISbkCwVBEVA0lASlQOmQGFJAi6FVUDFUApVDu6Eq6CjUBJ2DLkOd0AOoG+qH3kJfYAQmwzRYHzaHHWAmzILD4Vh4DpwOz4fz4QJ4HVwGV8KH4Hr4HHwVvgN3wa/gIQQgKggdMULsECbCRiKRZCQNkSJLkSKkFKlEapBmpB25hXQhA8hnDA5DxTAwdhhvTCgmDsPDzMcsxazFlGMOYOoxbZhbmG7MIOY7loLVw9pgvbAcbCI2HbsAW4gtxe7D1mEvYO9ge7EfcDgcHWeB88CF4pJwmbhFuLW4HbhaXAuuE9eDG8Lj8Tp4G7wPPhLPxcvxhfht+EP4s/ib+F78J4IKwZDgTAgmJBPEhJWEUsJBwhnCTUIfYZioTjQjehEjiXxiHnE9cS+xmXid2EscJmmQLEg+pFhSJmkFqYxUQ7pAekx6p6KiYqziqTJTRaiyXKVM5YjKJZVulc9kTbI1mU2eTVaQ15H3k1vID8jvKBSKOcWfkkyRU9ZRqijnKU8pn1SpqvaqHFW+6jLVCtV61Zuqr9WIamZqLLW5avlqpWrH1a6rDagT1c3V2epc9aXqFepN6vfUhzSoGk4akRrZGms1Dmpc1nihidc01wzS5GsWaO7RPK/ZQ0WoJlQ2lUddRd1LvUDtpeFoFjQOLZNWTDtM66ANamlquWrFay3UqtA6rdVFR+jmdA5dRF9PP0a/S/8yRX8Ka4pgypopNVNuTvmoPVXbX1ugXaRdq31H+4sOQydIJ0tno06DzhNdjK617kzdBbo7dS/oDkylTfWeyptaNPXY1Id6sJ61XrTeIr09etf0hvQN9EP0Jfrb9M/rDxjQDfwNMg02G5wx6DekGvoaCg03G541fMnQYrAYIkYZo40xaKRnFGqkMNpt1GE0bGxhHGe80rjW+IkJyYRpkmay2aTVZNDU0HS66WLTatOHZkQzplmG2VazdrOP5hbmCearzRvMX1hoW3As8i2qLR5bUiz9LOdbVlretsJZMa2yrHZY3bCGrd2sM6wrrK/bwDbuNkKbHTadtlhbT1uxbaXtPTuyHcsu167artuebh9hv9K+wf61g6lDssNGh3aH745ujiLHvY6PnDSdwpxWOjU7vXW2duY5VzjfdqG4BLssc2l0eeNq4ypw3el6343qNt1ttVur2zd3D3epe417v4epR4rHdo97TBozirmWeckT6xnguczzlOdnL3cvudcxrz+97byzvA96v5hmMU0wbe+0Hh9jH67Pbp8uX4Zviu/Pvl1+Rn5cv0q/Z/4m/nz/ff59LCtWJusQ63WAY4A0oC7gI9uLvYTdEogEhgQWBXYEaQbFBZUHPQ02Dk4Prg4eDHELWRTSEooNDQ/dGHqPo8/hcao4g2EeYUvC2sLJ4THh5eHPIqwjpBHN0+HpYdM3TX88w2yGeEZDJIjkRG6KfBJlETU/6uRM3MyomRUzn0c7RS+Obo+hxsyLORjzITYgdn3sozjLOEVca7xa/Oz4qviPCYEJJQldiQ6JSxKvJukmCZMak/HJ8cn7kodmBc3aMqt3ttvswtl351jMWTjn8lzduaK5p+epzePOO56CTUlIOZjylRvJreQOpXJSt6cO8ti8rbxXfH/+Zn6/wEdQIuhL80krSXuR7pO+Kb0/wy+jNGNAyBaWC99khmbuyvyYFZm1P2tElCCqzSZkp2Q3iTXFWeK2HIOchTmdEhtJoaRrvtf8LfMHpeHSfTJINkfWKKehzdI1haXiB0V3rm9uRe6nBfELji/UWCheeC3POm9NXl9+cP4vizCLeItaFxstXrG4ewlrye6l0NLUpa3LTJYVLOtdHrL8wArSiqwVv650XFmy8v2qhFXNBfoFywt6fgj5obpQtVBaeG+19+pdP2J+FP7YscZlzbY134v4RVeKHYtLi7+u5a298pPTT2U/jaxLW9ex3n39zg24DeINdzf6bTxQolGSX9Kzafqm+s2MzUWb32+Zt+VyqWvprq2krYqtXWURZY3bTLdt2Pa1PKP8TkVARe12ve1rtn/cwd9xc6f/zppd+ruKd335Wfjz/d0hu+srzStL9+D25O55vjd+b/svzF+q9unuK973bb94f9eB6ANtVR5VVQf1Dq6vhqsV1f2HZh+6cTjwcGONXc3uWnpt8RFwRHHk5dGUo3ePhR9rPc48XnPC7MT2OmpdUT1Un1c/2JDR0NWY1NjZFNbU2uzdXHfS/uT+U0anKk5rnV5/hnSm4MzI2fyzQy2SloFz6ed6Wue1PjqfeP5228y2jgvhFy5dDL54vp3VfvaSz6VTl70uN11hXmm46n61/prbtbpf3X6t63DvqL/ucb3xhueN5s5pnWdu+t08dyvw1sXbnNtX78y403k37u79e7Pvdd3n33/xQPTgzcPch8OPlj/GPi56ov6k9Kne08rfrH6r7XLvOt0d2H3tWcyzRz28nle/y37/2lvwnPK8tM+wr+qF84tT/cH9N17Oetn7SvJqeKDwD40/tr+2fH3iT/8/rw0mDva+kb4Zebv2nc67/e9d37cORQ09/ZD9Yfhj0SedTwc+Mz+3f0n40je84Cv+a9k3q2/N38O/Px7JHhmRcKXcsVYAQQeclgbA2/0AUJIAoKI9BGnWeI89ZtD4f8EYgf/E4334mKGdSw3qRtsjdgsAR9BhvhwANX8ARlujWH8Au7gox0Q/PNa7jxoO/Yup8UK0Vjk9ta0C/7Txvv4vdf/TA6Xq3/y/AOOhDyne6KAWAAAAimVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA5KGAAcAAAASAAAAeKACAAQAAAABAAACdqADAAQAAAABAAAAigAAAABBU0NJSQAAAFNjAAAAAAAAAADxh4F4AAAAHGlET1QAAAACAAAAAAAAAEUAAAAoAAAARQAAAEUAAAbT33OL9AAABp9JREFUeAHs3F9olWUcB/DnLHCT/rgKQxbhtLwpkIqsLrxZQfQXKggEA/tjZuCFCRHR1Wg3XiyhoKgVeKFd1k1CFNGNRAhhkFAQFBlSkLhjbqtNbW3jeOB0dt6dHc905/d8dnXe5332nvf3+b7jfGXMUt+NG6aTLwIECBAgQIAAgY4XKCl2HZ+hAQgQIECAAAECcwKKnQeBAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEoPT+4NHp+WY58edPaeST1+c7ZY0AAQIECBAgQGAZCpQ+H/ln3mJXPnMy7R4eWIa37JYIECBAgAABAgTmE1Ds5lOxRoAAAQIECBDoQIFFF7uelb0dOKZbJkCAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATgYbFburcZNoxdFcdQ8/K3ro1CwQIECBAgAABApdfoGGx+3d6Oj03uLHuDhW7OhILBAgQIECAAIFlIaDYLYsY3AQBAgQIEMhLYOvWp5dk4IMHDyzJdTvloopdpyTlPgkQIECAQCABxW5pwlTslsbVVQkQIECAAIECAcWuAOciTil2F4HnWwkQIECAAIHWBBS71twW+i7FbiEh5wkQIECAAIG2Cyh2bSedu2DDYnf2/Nn0wht31r2rv4qtI7FAgAABAgQILFJAsVskWJPbGxa78pmTaffwQN1lFLs6EgsECBAgQIDAIgUUu0WCNbldsatA3XrbxnTfA4/VsH343r7098REzVrRQXd3d3p+58upq+uK6rYD+99N5dFT1WMvFhboX3dzevTxLdWN4+Nn0v6Rt9P0zP+t6IsAAQKdIuAzoTgpxa7Yp9Wzil1FbuD+B9OLu16pcdzxzJPpr9Ona9aKDtb2r0t7931Qs2Xv0Gvp6LdHatYcFAs89MgTadv2XTWbtm15OE1OTtasOSBAgMByFvCZUJyOYlfs0+pZxa4ip9g1foRW9fam/nW3NN4wc+b8uXPp2PffFe5p9qRi16yUfQQILGcBxa44HcWu2KfVs4pdRU6xa/wI3X3v5rTn1cHGGypn3hoeSl8f/mrBfQttUOwWEnKeAIFOEFDsilNS7Ip9Wj2r2FXk/l/spqYm085nn0oTE+NN20b9IW622I2882b68otDTXs12qjYNZKxXiSwevUNqdTVlaZmfmVfLo8WbXWOwCURiPqZ0C48xa5dkrXXUewqHrO/buzru6mqMzrzBw9//H6ietzMi+6enrR+/Yaarb8d/yWNjY3VrLXjYLb8XHnV1dVLfXbo44t6n7X969OmezZXr3f815/TkW8Ozx3ffsemtP2lPdVzF15cs2pVWrGi+8Jhalexu/a669OaNX3V686++PGHYx3xxxNFjjUDOWirQKlUSvs/+jTN/gyWy6fm/lHW1jdwMQItCFzKz4QWbu+yf4titzQR/AcAAP//YCg3bwAAJAVJREFU7V0HvNXE0x1p0osivYMgIB2xIXb5oyKIoCAovUuvgvTee5WOFBEQULBgAxERQUBAlN5BQOkKCvrNCW7e3tzklpd738v73gy/R5LdzWZzNjc5Ozsze9unb1/7l2zkwuVz1H7U4345KVNl9EuThLhFIGu27DRuyjvmRU8cP0LdOzanv//+20wLd6dVu+5U6bGnzdMmjxtK679aax7b7aRPn4Gmz11uZr09eTR9vna1eZwYd2KDY2LEKdL3nCVrNho/dYFR7fXr16l+7WcjfQmpTxAQBCKMQN269SJc463qFiyI+T5G5QIer/Q2IXYe7yGb5tWoVY9efrWhkXPz5k3q1a01HTywz6ZkaEkpUtzOBG0ZpUyZyjhh86YNNHpYn6AnC7HzhSi2OPrWErdHadOmpatXr9K//9qO7+K2MS6uVrb8A9S15yCjhvggdv9fcHTRBYnm1HTp0tHly5cTzf1G80aF2EUH3URD7PIXKES33XZbyCheu/YnnTxxPGD53HnyUvLkKQKWiS3hypkzN2VhzVzmzFkp0x13Egjc2TOn6fSpE9SmQw+6K2t247pLFs2h5UvmB2wDMlOlTk158uSnzHdlpTvvykKpUqWmSxfO06+nT1KuPPno1debGXVc5LQu7RrRpUuXgtYZCWKXNGkyypsvf8Br/fnnH3Tq5ImAZVRmnrz5KVmyZHSDtZdHjx5WycY2W/achOcAcsedd/G9n6Bf9uwK6yUdDRyNBrn4D23KzvdmJ+fOnaFLFy+aWbfffjs9VbkqFSlawsDirizZ6N0Fs+j9pbe0XWZBm50778zMfVWQUqZKRRkyZKKzZ0/T0SOH+Ln8NWximJKfv/wFClKWLPyM8zMJuXDhd7rIf6f4d3fixDGbFtgn4Z5q1m5AVau/bBbo0bmFuW/dOcbPhZ12Oy5wTJ48OeXKnZfy5i9EWfg3fP63c4zhAeNZ/fOPP6xNDek4Y6Y7KD/XlztfAUqaJClBg3/k8AE68+vpgP2C90omPhdy7OgRxuQvSp8hA5Up9wBdv3aNU/+lrd9vMtJRpmChIlSocFG6fOkC3bhxg3Zs3/JfOeRGT25PmZLwPoTg/fQbYwbB+6fYvaX4PZmDf/PJ6ejh/XRg/146//tvRn6g//AsZ8iYybbI8WNH6a+/rpt5KPt0lWpUoGBhfmbvprTp0lO/nu3pZ353WCWa3wT0c15+v+Hdf8cdmbmNf9H587/RxfO/0+FD+/n3c97anKDHbp/HLFmyGnjgQngX4LlQgmep0N1FCe3+959/jPbu2/uT8VyqMkLsFBKR3SYaYjd38RrCByBU2fvzLur9ZruAxcdOnkvZsucKWKZOjacCvlytJ5e770GqVqMOFb6nuDXL7/jo4YPUvVNz+od/NE6SkV9ez75Qi57mjzk+XMFkxOC3+GX+bbBiRn4kiB0+LlNmLgl4vT27f6R+b3UIWEZlTpy+iIlCFgIxb1DneSM5W/YcVLteE7r/wUp+5P7K5Us0Y+pY2rRxnarCdhtNHG0vGEZimbIVqFuvIbZnrHr/XVo4b7qR91DFx6hu/eZ0Z+YsPmU/WbOCZr89wSdNP6jwQEVq1LwdZcx4iwToedjH4GD8qIH8Uf3FmuV3DK1m5Wer8zNe2/wg+BXihLO/nqIftn5Hy96dx4OMGGKqyqJNjzz2DA9W8hkf9nAGbWs+WEbzZk1WVZnbaOKId0/9xm/Qo09UpqRJk5rXVDvQmH752Uc0d+ZEgsYxFCnOpKZl224mMbae8/tvZ2kSm1Ts3rndmmUcv1ynAdV4+TVjf1DfLnQb/2vTsSelY8KkZOeOrTRicC9qzP2Ptuty6MBeGtyva1gDI/38UPeL3FOM+g259Xyu//JTmjpxBL1StxH977katu/03Tu30dgR/QK2CwPZF158xbYJwwa8Sdt+2GwM2jFYqPbSq37XGTO8L3337dd+50fjmwBi9NIrr9MTT1XhZyeZ3zWRgOcH/bGF392hDPQj9Ty24uev0uPPGG3q2aWlQaxTp07DmNWhKs/XIPzedYGCYuXyRfQeKyTQZiF2OjqR2xdi54BlfBC71xu1omervuTQIv/kQwf30ZudnDUT2XPkpL6DxjmOTP1rJK6vOR06uN8uyy/Ny8QOjW1WvwZrRbJRj74jCC8bJ8HLplv7JnT8+FHbItHG0faiYSQGIiRbNm+kqROGUafuA6ho8ZK2tToRO2g+6zVowR/QF23P0xNv3rxB82dPpY9Xv68n++yDfGG6tEy5+33SAx307NLKljCCaDz9vxcCneqY9/W6z2jSWH8iHC0cc+XOQ+079zE0446N+i/jFGsqRw3t7fgsohhwfIkJGT72wQgtPp4rli00tLLWa+vE7tOPVtEjjz5lO/g7d/ZXR/IY6oyB9drhHOvEDu+848eOGG0NVMcZHhgMHdDdcdYlELED6f/5px+pQ9e+BI22ncQVscM7dvDIqcZg1a4d1rRQzBAi+TzqxG7siP70065t1Kv/KMqdt4C1aT7H0yePoi/WrhFi54NK5A4SDbHr1X8kTyMUC4icrtELhdgNH/M2ZbVMgel14GK1X3wy4DVVJrQpbTv1UofGFtNFZ8+cIkxFpk6VxpiatY7YnBwW8MIfOX4m5cyV16fOC6y6P8+q+yScn4nV+ekz+DrDYJTfummdkLSMkSB2GI3qjiCqsTqOe3bvYI1dR5UVcKs0dig0c9o4qvNaE5PUXblymV/Yu3ha+waVKlPetClE2Y0bvmKt0wDs+khc4OhzwVgclCpdjjoycVOiYwfN1xmewi9eoozKNraYdjvGUycXL16gbzd8aeso06RFB562vaX1VCfjmfydp3czZrqTMEWmCzTHnds2dPyYPlPlBWrUzFcLjg/ROW4fpr4wxYXnQTdvcCJ29Ru3pieefs68vH7PSAyk9dr49Rc0bdJI81y1Ew0ccS/jpsznqf/M6jLGFu374+oVw8zCJ4MPftmzk/r0aG9NNo9BtBs0ecM8VjuYgkydJq2fdgn5g/p0pp0/blNFja1O7FQGNN3QsiRJkkQlmdur/PvBVJs+hYlp305tGpllorGjEzu9fpBWmKZAMBth1ShD2ziob1f9FHMfNsrP8UyGEv352bZ1s2EmgGdcF7w/TvLgD1OeK5ctMLRTej72I/lNQH3tu/SmBx56FLumoB2/8W+QX9L8/s5k9Ifqr2DELtLPo07soIkry4M2Rerwnv1p1w7WnF6iIjwDpc8UwOyiRcNaQuzMXo3sTqIhdqHA1rFbP8IUDyQUYmdXZ3VW29eu19jMCnUqts/A0axRKWWet2LpQlr1/mL644+rZlr69OnZlqgh4QOpZNPG9ca0gzpWW0zT9BowWh2yXcMpg7js3xczXQbSUoy1OCCU+ssaNnawuQkmkSB2Ttfo0WcYlSxd3siOzVSstd5PPlpJSxbMNBwFkAfP4mFMzJXDCDQlHd5oYD2N4gJHv4u6THj4kSeMKTW7ajDlvO7zj2k3v3B1OyJrWdgjjpow25w2xIt4+sSRtH3b98bUP6YT87JNFzQf95Ysa57uRJBRoPeAUWwTVdosu3D+2/TpmpXGtLlKxDNZuEhRKn9/ReODBs1IKHaqtes2puo1XzWqCfZxU9cKto0Ejs+9UJNea9jSvBRIw9LFcwybKGiKYYdUslQ5atGmm2Ebqgo6mUSAgMD7V/1e8fGcxv2yY/v3bH92wegv2JnWqtOQ4FCi5CBPk/fs2tpnwGYldqdOHqeBvTsZpK43vzuUHS/qwHsGnvJoc9tOb7FZwyNG1ZHCWrXTbmtH7E6eOEojh/TyGUQ88FAlgle6Pv0X6gxE05Yd6clnYgYKqh0YyHy8ejlt/nY94d0JMhmuxPabgPuYMf99837wLZg8bgj9sOU7H/MblCtZuiyVr1CRSpQqawzMndoY6edRJ3b6NX/atZ2mc5QERbzxnsXvvwDbaSpp0bAmPfecP+Yq381WvGIl3In5/MQnsZuz6EOTZOAl3IOnoOwEH77xU98xpwhg39Su5S07Gb3889VqGdNoKm34wB6GzZI61rewnWnZJmZki2kqTFcFk4RC7EAgVi1f7Hc7TVvxy/w/rQ9e4K+/UsXvxR0XOPo1zGWCHSH5lTUbs6aPYwKwNaTa23XuTQ8+fEtTAGzat6xnGq3rFcD2cMS4maZdFj58bZrVoXPnzurFjP05Cz9gx4vUxj5e/P17dfIrE9uEuCJ24eCIe53Av1Vls3bu7BkOS9SUrly54nebsEeCFlLJ3p93s41vW3VobmF/Cy20kvmzp9DqVUvVoblNkyYNDR093XxPIGMwa69+ZC2WEiux07WjLd7oQo89+T9VlJq8Vs1s90MVHzfIncqELSs0fdESO2LXsvHLtk4S1ncZbPImjx8WtGl2xG7Xjz+w1n9syI5bTheJLbGDo9eQUdPMapcunktL2eY0thKN59GO2H3/3Tc0bmR/H0cKtLlipSfojQ49zeb3ebMNlS8XMyg0MyKwI8ROiJ35GMUXsQNZW7hsrWkvE0xbOGbSXMqe45bTBj407Vq9bt6D2qnJ9jc1a9dXhzwl0YV27vjBPNZ3rERg4pjBtGH953oR2/2EQOzg7QmvTzuB8bTyBkZ+3ZqVjWlavWxc4KhfLxL71v4ESZ8+aZTp3RjsGpjWmvXOKvN5XPfFJzRlwnDH0+AM0bBpGzPf6VmbtWCVOS0O78ZWTV4xNEDmiS524oLYhYtj+QoPUuc3B5p3NXxQT9a2bDKPrTuDR0w2NRrQzjSq+4K1CA0dNZXysWcmxE4Lp5+gh4BBupUEWomdbjYC8ggSqUTPK1GyDPXsFzOVDVtWOwcXda7brZXYbdn8LWvr3rKtFhrNabOXmgMIOJh17dDUtqyeaCV2E8cM4nfgF3qRWO/Hltjly1/QIOfqwnDWgAY7thKN59FK7DBgw/Q3NLtWKXR3ERo4PMZpCQONEvcWtRaLyLEQOyF25oMUX8QODRjJWg+EHYFA6zFj6hjDuFRX/cO+7rlqNenV12JeVNvYc3AYa+OsgmmJ9l36mMnwmMLUhQoVoDIwJdm911CTKCJdeTepMk7bhEDsAk2FW22V7IhdXODohG9s063Erm2Luj4hBoLVa9UUDBvIXoI8hegk1vJOdp86KUFdsIGaN2tSSNP+TtdW6XFB7MLFUdfC4XfckInaNbaXdRI4qkBDrETXkqm0We+sNOzocPzBindpwdxbHs8qX99aCTocW+bMmGgW0YkdbDHbtKhn5j3Kno7wuIWgzQ1erWrm3VP0Xuo7eJx5HNfEDs4N8Gx2ki49BhKiC0BgF9j4tepORc10ndj9zuFUMOiIlMSW2MHhC4MhJXiG1n78AU/lzw4pHJU6T22j8TxaiV3T16s7eiPDRGD42BmqOYYGWYidCUdEd8TGToMzPomdnaE6HB2OHD7ExtApjLhb+ThWlZrWUc2GBx1U31ZB7KUJHPpDGdUiHy+GA/t+ZsPya6wmZ/settnD6B8aQyWIf9Wtw62YdirNaet1Yvcl25JN49AIThIKsYsLHJ3aF9t0t8TOSmZhi3fk0H7H5qRNl8FnYAD70MVsz2gVK94qH/aNMOyHRx1CVcQm+KsXiV19dnCo8p9HMRwbMH0YSODlC29fJdYBFoIgz5i/UmWTE4E2C/DO9DnLTAcp6yBQJ3bo3268eo0SLxO7YNq0Zq06sWPNs+pWjLBHwaaKdWKHKfM32JwgUhJbYofr698k1R44KUFbu4t/M7v5NwOHMDhDBZNIP4+4nk7sgpFoIXbBeihy+ULsNCz1H1Gw6VDtNJ/d2P6I8dIeMHQiZf8vEKdPpQ4HiHsFt3Fdq6cXtfNC1POt+/hh9u/VkcnkQWuW7bHXid1HHy7nuGCTbNuORCvRsNPYoVy0ccQ1Iiluid0LHGNO1wqH27bPPvnQ0Dhbz4PDRa06DYwpPn0woZfDRwteoXAcCqQl1M/BvheJna45cnLO0e8DS/rB+F/JkP7daMe2LerQCCit21whduBG9mgOJLDHRSBkyH4ODvtWt5gp84RK7Ky4WO8fzmt4Dytpz6YqyohfpVm3XiV2GHzDsUZ3hLG2HcGkEXdvycKZPs4k1nKRfh5Rv07sgikFhNhZeyR6x0LsNGzjk9ihGXdxYN1GzTtwnK8KWqv8d+Hh+sGKJfTZJx84kjp1FpwD4CQQSPAxhTfj+0vmhRXxP7EQO2AXTRwD9U1s8twSO+uUYLhtgAfy7OnjHU+DpzFisN1TrKSPRtl6wtqPV/FU7ZSQtBFeJHb9Bo81VvjAfZ0+dZzat4qxebXeK46thv9dObYiovkrsdq2BdNc4byJ0xeaMeisSwUmVGIH+zrY2TmJ/iygjJOjhX6+V4mdaiPCDj31zPPGiiVOgyJ4KL/DzjRr+btgJ5F+HnENIXZ2SMd/mhA7rQ/im9ihKbrHGZYY2rrlW8NOBPGjECj0NIckwFI+IGOhij5q//zT1XSDQySk4PhaqA9LTu1hg9czvCxUuGIldsFsX8KpPxLhTiKlsVPtjhaOqv5Ibd0SO2tIBESy3/fLTyE3D0vfOQV71ivBmpvFS5Tlv9JGaJusvDSUVUINgKt/zCMVgsMtjnoMMjiLNOfwDoEEwckRpFyJ1dsUgWVHjp+tsmkmr5ji9BFXhXSbvA9XvkfvzJmqsiihEjvEIMRshZPoJA1G/PVqVQ46ANbP8dJUrPUe4YVeileaKVXmPirBYYaspjkoj+XO9vy003qqT0y8SDyPuIAQOz+YPZEgxE7rBi8Qu85vDuB4RA8ZrRrA06KIN+ZGdM+qcAL9hnJNrEwwf8nHpo2e9cMRSh1OZbxG7KKJoxMGsU13S0hgeI5pGyWhekmr8rHdYs3g1xq28omLF2oAXJ3YYRCEj7lbcYsjprMxra0kkGE5yui2YZd5GbWm7G2qC+KVzV282vy9WZ0h9LLYh33opBnvmsnQokKbqiShEjuE/EDoDyeBJzI8QCGhkrSEQuz0e4bmDlO0r3OcRD1QPjTdCM5ulUg/j6hfiJ0VZW8cC7HT+iG+iR28oKaxsTMWZoY0a8BhBLQF3LWmhryrhy1w+sGHXJlNwcn84VBR9SNJHL1G7KKNow20sU5yS0hy5WLNEAcnVuIUU03lR3ILz++ps5aYmohQtW/WsDQIfhqbRdH1e3GLI6bP4BSlZArHU1vHcdXsBKt4jGJtHNY5huzfu4ft4fxXl5g6+z1zhQXY7XXh6Vp94XW9bmsYmqH9uxsBplWZhErsjh89TJ3bxQSBV/eDLXA0wp1wQFxIqM9uQiR2xg3yfxgQDRsT420K21R4slslGs+jEDsryt44FmKn9UN8EzvdEw3NQtwieLDi7y+2n7jEyz9hibFjRw/xeolHg04voI4J0xb4BCmFsTU+ltc5oOi1P/80lqaB/Q/WYLQLnIo6Akn/IeOo8D33mkXe6trKiNBuJvy3A+eQv1mTAkPfUMRrxC7aOIaCSahl3BISaGInz3yXvaZjlptDwNFvv1nn2IQCBe82FmVfveo9R+cbxLFCQO1AXq/w4h41fpbpRATP8BaNAnuTolHWe0ZYj0Dr1jreiJZhrTPccCe6lhfVIjZd947NbEPPNG/dmR7nRd6VIG4g4gdapVP3/nTf/Q+byU5auxw5c9GQkdPMZd8Q77Jjm4Y+8cUSKrHDzTvFSqxcpRo1bBYT2Bne2fDSDiZeJHYgqblz5zV+TwgS7iRYhm/qrPfMbMRKRMxEq0TjeRRiZ0XZG8dC7LR+CIfYQbuWiX9QVqlWsy7Bu01JZ36Z6l6rsG/79fQple2zta5y4JNpOcAHb/Omb+iD9xfRWXbPtxNoP6bNWUpp06azy/ZJU96IWJgZwYn1NvsUtBxgzcUatWLiX8GzFtHaf2Q7QNSBtWof5Qj2lR57xlj6bOv3vkbPeCmlYSyt0qPvcHNtQRBa2I3ocpU/khd4zVur6GvFRsrGLi5wtN5HOMdwutGXUXq40pNUgxeJV4LR+xmHZ+4k22za9bWdswjsudZ9/pHhYIO1hhEkOwdr9x59vLK5Fu0AXpJq987t6tI+W9h74WP14/YfaNM3X7JjwEEeWJw1gtsivWDBwlT1xTo+zkNbNm804i/6VGRzYNVaIPYaAlNv5OtA6w17vrwcLqjCg5VoB3sQol6rRANH/Z2C6x3je/549Qra+8suXgLsPBW6uyiVYHspFRYFZbBcVue2jW3taLGMG1aU0A3oYQKxi2MCHtj/s6E9L1zkXsPjOyeTAiV2jhYJmdghBA+mG7d8t8FYJhD2vlg7+JW6jUxsMIBt3eRlvwErsMueIyfdxv+UwMEMzjxKOtksL4g8hBVxskeO9DcBfY1lDzEg+H7TBg5rtYG/HSf4fX/WiC2IwXIJXo4OnuY5cuZRTWfv2Nm0/L13zGN9J9LPoxA7HV3v7Aux0/pC1xIhntaA3p21XN/dQSMmUcFC9/gmhnBkDQSqn4IXDpbygXdcqIIXzdgR/clKmNT5CB7bq/8oM6ipSg+0xb0PHfCmETsvUDnkYekirF2ZJgTyaF3/Ekvc4GOvx9oLdj2V/xXHqJtqE6MuGsQO14w2juq+wt1aDerDPb9+7WcNDa71PDyLQ0ZOMVc5sOY7HQcjdlik3iogljpR0fOd4jTqZdT+m72HGkbl6thpixUksGyeLtHCEdPawzn4eDjP+OhhfXjQtkFvns9+m449DQ2lT2KAA8So696phR+BT8jETr9dzAJgYGAVJ+9sa7xA63mBjkGee3aJWfpNLxvpb4Iidvo11L7TbwaEF8oEJ/IZ6edRiJ3qEW9thdhp/aFPtwULbjts9DRDA6CdHtJuIGKHCvCBK1m6HAclTmloYVLwEjlp06WnrFlzGAvXI0gxjnXBi63jG/X9VpVQZbJlz2FozlKkSGHUiZdg5sxZKQt7IWLkmidvAb8P60ccpX6uFqVe1WW3xRRys9adeAHyZHbZZpqV2Fkjq5sFQ9iJa2KHJkUbxxBu26+IVVPlVyBIghOxw2nQYLXgNYSLlygTpJZb2dCsYhH5o2wDZSe6h6ZdvjUNWgdoH0IVtHcQk1F9CtnuXDtiF00cKzxQkTDVGmzwc4W13VMnDAsYygP3A01Ns9ZdCPUGE5hzTJ80wtBqWcsmVGIHu7nC9xS33o7P8aaN6w3ybhe4F9pRBOuNjQQidpH+JgQidnZthwfw2BH9bAPW6+Uj+TwKsdOR9c6+ELv/+gIhF6DZUrJo/gxauXyROvTbWm3L/Ao4JFiDhDoUc0yG/VP5Cg/zGqdNzcCjKAwNBD5YsREQlipVaxLsU5TAFqpdy5jpPJXutIVGCx8vTHdZtS8Iq7Jh3ee05sOlPs4gIJgz5q0wnUWc6rZLR9+gj6wyasIsg8QiPZyp2CuXLxleiHbTktZrOB1HAkenup3Scc0xk+b5Ye5UXk/HtHmzBi/52F3p+dhHXyJ+FkJxwPPOqnmC4f6BfXt4qaNVhI+pkyE/6sIg4v4HH6X7ebm7/LziiRJMmWGNTwjwx+LrCLFiF7JBneO0Bel5+dXGxsoD+K1Y5TcO74MYkFb7u2jjmDnzXWz71Y6KFithaM+hWVHT5zCr2PvLHpo3cyKHHzprbbLjMTRP6Jds3C/oJ1Un+uA42+F+zmYV6Bcn0ddKRiBkBP5VUpqnh7uzBhQSaEkxDCrh5IU+jJZY14rtxU4lOTiQe3U2e4E5AOzP0NfAAP0LB5X3Fs3x01Cq9sGWEe+q2IiT/RrqivQ3AfdTuEgxw3zgfjYhUI41uJbqa+yjv9fzPa9YtsDWfhNlrBKp5xErpeA5hIQboLhbhyZU8eEYe1FrG90cy1qxslYsL7mTgXr3H22u1Qp7s45sYxEsWrmbB8/tuU9XrkqNW8TYncGeCAvex1bwEpnC3ogZM96yG8Tor27NZ8KuDmQNBr9JkiTlF+2tcAO/83JKbghT2I2IxxMihWM83oLjpfHxxAcV2mR4biNQNtYejk3fpmSvxYyZMlGGDJnYhuiK8dzBVhQ2d3ZaFsdGOWSgH6DBAxkFaYSd3fnfzznaozpUE5VktCtlqlSseUtPJ44fNWwM3VwI95crdz7+2F/j31wSrvMYk/Ubbqr01Ll2xG4few2jjxH7MEOGjAbROc82t3Z2t566GReNwaAF7+cMbJd8mbXjMGvAoBnv13DimlqbEOnn0Vp/oOO6desFyo51nhC7RE7sSpQqS42atjW98PAkrf9qLU0ed2u0GusnK4onYtqodbselIeNa5UEi8auytltoTnAyF83PD7MXrKwyxEJHQHBMXSspKQgECoCTsQu1POlnHcREGIXnb5JVFOxWKsSxv7wICpavBSVu+8BKlS4mA+yWKwbITugiYhPAUmAJ19a/kvPWg1MOeTkUXmx4iUpd578Pk3DVE4nNpi9evWqT7p+gNEt7h0LtqdjGz2o9eGxmidfQSpVuryf8fHC+W/TquWL9SpknxEQHOUxEATiFgEhdnGLd1xeTYhddNBONMRuxLgZfoTICikCfg7u1zXOp2uwjFgjtlVIniw5JeXpLhBQEIhQBK7wMFg/eGCfT3F4NIIM3qovmZ9tlE9hywE0lgimGpspNktVCf5QcEzwXSg3kMAREGKXwDswQPOF2AUAx0VWoiF2cxevMQ20rXjBpgfG1CvYRi2Q1st6XqSOq1Z/herWbxZWdTBW/mTNCtaqLfSL04SKAt2v04Xg8bWEbfV2bN/qVCTRpQuOia7L5YY9hoAQO491SASbI8QugmBqVSVaYgdPql/27KQd276njV9/Ea9Tr6EQO0y3nmSN4pEjh4xgpFhDFt5qThKMkIDMwjkEhtZ7f95FO3ds4RUtjjhVl2jTBcdE2/Vy4x5BwLrEHUI7nTxx3COtk2a4QUCInRv0nM91JHZ/3bhOzQaW9zszZaqYZYb8Mj2cgKC/WEYLi2tfunTBcAuPpot+OFDAGaIgR6C/ceNvunnjprFFGIHLHILj8uWLdJE9oHAcjjzGqz1AbrIrPOoFkb169TIhrMclYMBegjLVGhxRwTE4RlJCEIg2ArA3hnkK3lmBlqSLdjuk/sgiIMQusniq2hyJ3T/8A2rUL2aJFXVCQiV2qv2yFQQEAUFAEBAEBIH4R0CIXXT6QIhddHCVWgUBQUAQEAQEAUFAEIhzBITYxTnkckFBQBAQBAQBQUAQEASig4AQu+jgKrUKAoKAICAICAKCgCAQ5wgIsYtzyOWCgoAgIAgIAoKAICAIRAcBR2L3982/qemAsn5XFecJP0gkQRAQBAQBQUAQEAQEAU8g4EjsLlw+R+1HPe7XSCF2fpBIgiAgCAgCgoAgIAgIAp5AQIidJ7pBGiEICAKCgCAgCAgCgoB7BITYucdQahAEBAFBQBAQBAQBQcATCAix80Q3SCMEAUFAEBAEBAFBQBBwj4AQO/cYSg2CgCAgCAgCgoAgIAh4AgEhdp7oBmmEICAICAKCgCAgCAgC7hEQYuceQ6lBEBAEBAFBQBAQBAQBTyAgxM4T3SCNEAQEAUFAEBAEBAFBwD0CQuzcYyg1CAKCgCAgCAgCgoAg4AkEhNh5ohukEYKAICAICAKCgCAgCLhHQIidewylBkFAEBAEBAFBQBAQBDyBgBA7T3SDNEIQEAQEAUFAEBAEBAH3CAixc4+h1CAICAKCgCAgCAgCgoAnEBBi54lukEYIAoKAICAICAKCgCDgHgEhdu4xlBoEAUFAEBAEBAFBQBDwBAJC7DzRDdIIQUAQEAQEAUFAEBAE3CMgxM49hlKDICAICAKCgCAgCAgCnkBAiJ0nukEaIQgIAoKAICAICAKCgHsEhNi5x1BqEAQEAUFAEBAEBAFBwBMICLHzRDdIIwQBQUAQEAQEAUFAEHCPgBA79xhKDYKAICAICAKCgCAgCHgCASF2nugGaYQgIAgIAoKAICAICALuERBi5x5DqUEQEAQEAUFAEBAEBAFPICDEzhPdII0QBAQBQUAQEAQEAUHAPQJC7NxjKDUIAoKAICAICAKCgCDgCQSE2HmiG6QRgoAgIAgIAoKAICAIuEdAiJ17DKUGQUAQEAQEAUFAEBAEPIGAEDtPdIM0QhAQBAQBQUAQEAQEAfcICLFzj6HUIAgIAoKAICAICAKCgCcQEGLniW6QRggCgoAgIAgIAoKAIOAeASF27jGUGgQBQUAQEAQEAUFAEPAEAkLsPNEN0ghBQBAQBAQBQUAQEATcIyDEzj2GUoMgIAgIAoKAICAICAKeQECInSe6QRohCAgCgoAgIAgIAoKAewQcid1fN65Ts4Hl/a6QMlVGvzRJEAQEAUFAEBAEBAFBQBCIfwT+D/zF7ZhlIKO3AAAAAElFTkSuQmCC\"}}]}]}],\"tools\":[{\"name\":\"read_screenshot\",\"description\":\"Capture a screenshot of the current screen.\",\"input_schema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":40}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-7\",\"id\":\"msg_017z3Dpfd8nim5vfCmcAQMFS\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1005,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"j\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"iggling restroom prison\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1005,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":13} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json b/packages/llm/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json new file mode 100644 index 00000000000..8ab0cc2f1c9 --- /dev/null +++ b/packages/llm/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "metadata": { + "name": "gemini/gemini-2-5-flash-image", + "recordedAt": "2026-05-19T21:56:56.083Z", + "provider": "google", + "route": "gemini", + "transport": "http", + "model": "gemini-2.5-flash", + "tags": ["prefix:gemini", "provider:google", "media", "image", "vision", "golden"] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse", + "headers": { + "content-type": "application/json" + }, + "body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"The image contains exactly three lowercase English words. Read them left to right and reply with only those words.\"},{\"inlineData\":{\"mimeType\":\"image/png\",\"data\":\"iVBORw0KGgoAAAANSUhEUgAAAnYAAACKCAYAAAAnmweyAAACKWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjYzMCIKICAgZXhpZjpVc2VyQ29tbWVudD0iU2NyZWVuc2hvdCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjEzOCIKICAgdGlmZjpZUmVzb2x1dGlvbj0iMTQ0LzEiCiAgIHRpZmY6WFJlc29sdXRpb249IjE0NC8xIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+at0SpgAACrhpQ0NQSUNDIFByb2ZpbGUAAEiJlZcHUFNZF8fvey+dhJYQASmh994CSAmhBVCQDjZCEiAQQkxBwa4sruBaUBHBsqKrIgo2qg0RxbYo9r4gi4iyLhZsqHwPGMLufvN933xn5s75zXnn/u+5d959cx4AFFOuRCKC1QHIFsul0SEBjMSkZAb+JcACTUACnoDK5ckkrKioCIDahP+7fbgLoFF/y25U69+f/1fT4AtkPACgKJRT+TJeNsonAIABTyKVA4CgDEwWyCWjfB9lmhQtEOWBUU4fY8yoDi11nGljObHRbJQtASCQuVxpOgBkVzTOyOWlozrkWJQdxXyhGOUClH2zs3P4KLehbInmSFAe1Wem/kUn/W+aqUpNLjddyeN7GTNCoFAmEXHz/s/j+N+WLVJMrGGBDnKGNDQa9Xrouf2elROuZHHqjMgJFvLH8sc4QxEaN8E8GTt5gmWiGM4E87mB4Uod0YyICU4TBitzhHJO7AQLZEExEyzNiVaumyZlsyaYK52sQZEVp4xnCDhK/fyM2IQJzhXGz1DWlhUTPpnDVsalimjlXgTikIDJdYOV55At+8vehRzlXHlGbKjyHLiT9QvErElNWaKyNr4gMGgyJ06ZL5EHKNeSiKKU+QJRiDIuy41RzpWjL+fk3CjlGWZyw6ImGMQAOVAAPhCCHMAAgaiXAQkQAS7IkwsWykc3xM6R5EmF6RlyBgu9dQIGR8yzt2U4Ozq7AzB6h8dfkXf0sbsJ0a9MxlZVAeDTNDIycnIyFnYDgKMpAJDqJmOWcwBQ7wPg0imeQpo7Hhu7a1j0y6AGaEAHGAATYAnsgDNwB97AHwSBMBAJYkESmAt4IANkAylYABaDFaAQFIMNYAsoB7vAHnAAHAbHQAM4Bc6Bi+AquAHugEegC/SCV2AQfADDEAThIQpEhXQgQ8gMsoGcISbkCwVBEVA0lASlQOmQGFJAi6FVUDFUApVDu6Eq6CjUBJ2DLkOd0AOoG+qH3kJfYAQmwzRYHzaHHWAmzILD4Vh4DpwOz4fz4QJ4HVwGV8KH4Hr4HHwVvgN3wa/gIQQgKggdMULsECbCRiKRZCQNkSJLkSKkFKlEapBmpB25hXQhA8hnDA5DxTAwdhhvTCgmDsPDzMcsxazFlGMOYOoxbZhbmG7MIOY7loLVw9pgvbAcbCI2HbsAW4gtxe7D1mEvYO9ge7EfcDgcHWeB88CF4pJwmbhFuLW4HbhaXAuuE9eDG8Lj8Tp4G7wPPhLPxcvxhfht+EP4s/ib+F78J4IKwZDgTAgmJBPEhJWEUsJBwhnCTUIfYZioTjQjehEjiXxiHnE9cS+xmXid2EscJmmQLEg+pFhSJmkFqYxUQ7pAekx6p6KiYqziqTJTRaiyXKVM5YjKJZVulc9kTbI1mU2eTVaQ15H3k1vID8jvKBSKOcWfkkyRU9ZRqijnKU8pn1SpqvaqHFW+6jLVCtV61Zuqr9WIamZqLLW5avlqpWrH1a6rDagT1c3V2epc9aXqFepN6vfUhzSoGk4akRrZGms1Dmpc1nihidc01wzS5GsWaO7RPK/ZQ0WoJlQ2lUddRd1LvUDtpeFoFjQOLZNWTDtM66ANamlquWrFay3UqtA6rdVFR+jmdA5dRF9PP0a/S/8yRX8Ka4pgypopNVNuTvmoPVXbX1ugXaRdq31H+4sOQydIJ0tno06DzhNdjK617kzdBbo7dS/oDkylTfWeyptaNPXY1Id6sJ61XrTeIr09etf0hvQN9EP0Jfrb9M/rDxjQDfwNMg02G5wx6DekGvoaCg03G541fMnQYrAYIkYZo40xaKRnFGqkMNpt1GE0bGxhHGe80rjW+IkJyYRpkmay2aTVZNDU0HS66WLTatOHZkQzplmG2VazdrOP5hbmCearzRvMX1hoW3As8i2qLR5bUiz9LOdbVlretsJZMa2yrHZY3bCGrd2sM6wrrK/bwDbuNkKbHTadtlhbT1uxbaXtPTuyHcsu167artuebh9hv9K+wf61g6lDssNGh3aH745ujiLHvY6PnDSdwpxWOjU7vXW2duY5VzjfdqG4BLssc2l0eeNq4ypw3el6343qNt1ttVur2zd3D3epe417v4epR4rHdo97TBozirmWeckT6xnguczzlOdnL3cvudcxrz+97byzvA96v5hmMU0wbe+0Hh9jH67Pbp8uX4Zviu/Pvl1+Rn5cv0q/Z/4m/nz/ff59LCtWJusQ63WAY4A0oC7gI9uLvYTdEogEhgQWBXYEaQbFBZUHPQ02Dk4Prg4eDHELWRTSEooNDQ/dGHqPo8/hcao4g2EeYUvC2sLJ4THh5eHPIqwjpBHN0+HpYdM3TX88w2yGeEZDJIjkRG6KfBJlETU/6uRM3MyomRUzn0c7RS+Obo+hxsyLORjzITYgdn3sozjLOEVca7xa/Oz4qviPCYEJJQldiQ6JSxKvJukmCZMak/HJ8cn7kodmBc3aMqt3ttvswtl351jMWTjn8lzduaK5p+epzePOO56CTUlIOZjylRvJreQOpXJSt6cO8ti8rbxXfH/+Zn6/wEdQIuhL80krSXuR7pO+Kb0/wy+jNGNAyBaWC99khmbuyvyYFZm1P2tElCCqzSZkp2Q3iTXFWeK2HIOchTmdEhtJoaRrvtf8LfMHpeHSfTJINkfWKKehzdI1haXiB0V3rm9uRe6nBfELji/UWCheeC3POm9NXl9+cP4vizCLeItaFxstXrG4ewlrye6l0NLUpa3LTJYVLOtdHrL8wArSiqwVv650XFmy8v2qhFXNBfoFywt6fgj5obpQtVBaeG+19+pdP2J+FP7YscZlzbY134v4RVeKHYtLi7+u5a298pPTT2U/jaxLW9ex3n39zg24DeINdzf6bTxQolGSX9Kzafqm+s2MzUWb32+Zt+VyqWvprq2krYqtXWURZY3bTLdt2Pa1PKP8TkVARe12ve1rtn/cwd9xc6f/zppd+ruKd335Wfjz/d0hu+srzStL9+D25O55vjd+b/svzF+q9unuK973bb94f9eB6ANtVR5VVQf1Dq6vhqsV1f2HZh+6cTjwcGONXc3uWnpt8RFwRHHk5dGUo3ePhR9rPc48XnPC7MT2OmpdUT1Un1c/2JDR0NWY1NjZFNbU2uzdXHfS/uT+U0anKk5rnV5/hnSm4MzI2fyzQy2SloFz6ed6Wue1PjqfeP5228y2jgvhFy5dDL54vp3VfvaSz6VTl70uN11hXmm46n61/prbtbpf3X6t63DvqL/ucb3xhueN5s5pnWdu+t08dyvw1sXbnNtX78y403k37u79e7Pvdd3n33/xQPTgzcPch8OPlj/GPi56ov6k9Kne08rfrH6r7XLvOt0d2H3tWcyzRz28nle/y37/2lvwnPK8tM+wr+qF84tT/cH9N17Oetn7SvJqeKDwD40/tr+2fH3iT/8/rw0mDva+kb4Zebv2nc67/e9d37cORQ09/ZD9Yfhj0SedTwc+Mz+3f0n40je84Cv+a9k3q2/N38O/Px7JHhmRcKXcsVYAQQeclgbA2/0AUJIAoKI9BGnWeI89ZtD4f8EYgf/E4334mKGdSw3qRtsjdgsAR9BhvhwANX8ARlujWH8Au7gox0Q/PNa7jxoO/Yup8UK0Vjk9ta0C/7Txvv4vdf/TA6Xq3/y/AOOhDyne6KAWAAAAimVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA5KGAAcAAAASAAAAeKACAAQAAAABAAACdqADAAQAAAABAAAAigAAAABBU0NJSQAAAFNjAAAAAAAAAADxh4F4AAAAHGlET1QAAAACAAAAAAAAAEUAAAAoAAAARQAAAEUAAAbT33OL9AAABp9JREFUeAHs3F9olWUcB/DnLHCT/rgKQxbhtLwpkIqsLrxZQfQXKggEA/tjZuCFCRHR1Wg3XiyhoKgVeKFd1k1CFNGNRAhhkFAQFBlSkLhjbqtNbW3jeOB0dt6dHc905/d8dnXe5332nvf3+b7jfGXMUt+NG6aTLwIECBAgQIAAgY4XKCl2HZ+hAQgQIECAAAECcwKKnQeBAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEoPT+4NHp+WY58edPaeST1+c7ZY0AAQIECBAgQGAZCpQ+H/ln3mJXPnMy7R4eWIa37JYIECBAgAABAgTmE1Ds5lOxRoAAAQIECBDoQIFFF7uelb0dOKZbJkCAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATgYbFburcZNoxdFcdQ8/K3ro1CwQIECBAgAABApdfoGGx+3d6Oj03uLHuDhW7OhILBAgQIECAAIFlIaDYLYsY3AQBAgQIEMhLYOvWp5dk4IMHDyzJdTvloopdpyTlPgkQIECAQCABxW5pwlTslsbVVQkQIECAAIECAcWuAOciTil2F4HnWwkQIECAAIHWBBS71twW+i7FbiEh5wkQIECAAIG2Cyh2bSedu2DDYnf2/Nn0wht31r2rv4qtI7FAgAABAgQILFJAsVskWJPbGxa78pmTaffwQN1lFLs6EgsECBAgQIDAIgUUu0WCNbldsatA3XrbxnTfA4/VsH343r7098REzVrRQXd3d3p+58upq+uK6rYD+99N5dFT1WMvFhboX3dzevTxLdWN4+Nn0v6Rt9P0zP+t6IsAAQKdIuAzoTgpxa7Yp9Wzil1FbuD+B9OLu16pcdzxzJPpr9Ona9aKDtb2r0t7931Qs2Xv0Gvp6LdHatYcFAs89MgTadv2XTWbtm15OE1OTtasOSBAgMByFvCZUJyOYlfs0+pZxa4ip9g1foRW9fam/nW3NN4wc+b8uXPp2PffFe5p9qRi16yUfQQILGcBxa44HcWu2KfVs4pdRU6xa/wI3X3v5rTn1cHGGypn3hoeSl8f/mrBfQttUOwWEnKeAIFOEFDsilNS7Ip9Wj2r2FXk/l/spqYm085nn0oTE+NN20b9IW622I2882b68otDTXs12qjYNZKxXiSwevUNqdTVlaZmfmVfLo8WbXWOwCURiPqZ0C48xa5dkrXXUewqHrO/buzru6mqMzrzBw9//H6ietzMi+6enrR+/Yaarb8d/yWNjY3VrLXjYLb8XHnV1dVLfXbo44t6n7X969OmezZXr3f815/TkW8Ozx3ffsemtP2lPdVzF15cs2pVWrGi+8Jhalexu/a669OaNX3V686++PGHYx3xxxNFjjUDOWirQKlUSvs/+jTN/gyWy6fm/lHW1jdwMQItCFzKz4QWbu+yf4titzQR/AcAAP//YCg3bwAAJAVJREFU7V0HvNXE0x1p0osivYMgIB2xIXb5oyKIoCAovUuvgvTee5WOFBEQULBgAxERQUBAlN5BQOkKCvrNCW7e3tzklpd738v73gy/R5LdzWZzNjc5Ozsze9unb1/7l2zkwuVz1H7U4345KVNl9EuThLhFIGu27DRuyjvmRU8cP0LdOzanv//+20wLd6dVu+5U6bGnzdMmjxtK679aax7b7aRPn4Gmz11uZr09eTR9vna1eZwYd2KDY2LEKdL3nCVrNho/dYFR7fXr16l+7WcjfQmpTxAQBCKMQN269SJc463qFiyI+T5G5QIer/Q2IXYe7yGb5tWoVY9efrWhkXPz5k3q1a01HTywz6ZkaEkpUtzOBG0ZpUyZyjhh86YNNHpYn6AnC7HzhSi2OPrWErdHadOmpatXr9K//9qO7+K2MS6uVrb8A9S15yCjhvggdv9fcHTRBYnm1HTp0tHly5cTzf1G80aF2EUH3URD7PIXKES33XZbyCheu/YnnTxxPGD53HnyUvLkKQKWiS3hypkzN2VhzVzmzFkp0x13Egjc2TOn6fSpE9SmQw+6K2t247pLFs2h5UvmB2wDMlOlTk158uSnzHdlpTvvykKpUqWmSxfO06+nT1KuPPno1debGXVc5LQu7RrRpUuXgtYZCWKXNGkyypsvf8Br/fnnH3Tq5ImAZVRmnrz5KVmyZHSDtZdHjx5WycY2W/achOcAcsedd/G9n6Bf9uwK6yUdDRyNBrn4D23KzvdmJ+fOnaFLFy+aWbfffjs9VbkqFSlawsDirizZ6N0Fs+j9pbe0XWZBm50778zMfVWQUqZKRRkyZKKzZ0/T0SOH+Ln8NWximJKfv/wFClKWLPyM8zMJuXDhd7rIf6f4d3fixDGbFtgn4Z5q1m5AVau/bBbo0bmFuW/dOcbPhZ12Oy5wTJ48OeXKnZfy5i9EWfg3fP63c4zhAeNZ/fOPP6xNDek4Y6Y7KD/XlztfAUqaJClBg3/k8AE68+vpgP2C90omPhdy7OgRxuQvSp8hA5Up9wBdv3aNU/+lrd9vMtJRpmChIlSocFG6fOkC3bhxg3Zs3/JfOeRGT25PmZLwPoTg/fQbYwbB+6fYvaX4PZmDf/PJ6ejh/XRg/146//tvRn6g//AsZ8iYybbI8WNH6a+/rpt5KPt0lWpUoGBhfmbvprTp0lO/nu3pZ353WCWa3wT0c15+v+Hdf8cdmbmNf9H587/RxfO/0+FD+/n3c97anKDHbp/HLFmyGnjgQngX4LlQgmep0N1FCe3+959/jPbu2/uT8VyqMkLsFBKR3SYaYjd38RrCByBU2fvzLur9ZruAxcdOnkvZsucKWKZOjacCvlytJ5e770GqVqMOFb6nuDXL7/jo4YPUvVNz+od/NE6SkV9ez75Qi57mjzk+XMFkxOC3+GX+bbBiRn4kiB0+LlNmLgl4vT27f6R+b3UIWEZlTpy+iIlCFgIxb1DneSM5W/YcVLteE7r/wUp+5P7K5Us0Y+pY2rRxnarCdhtNHG0vGEZimbIVqFuvIbZnrHr/XVo4b7qR91DFx6hu/eZ0Z+YsPmU/WbOCZr89wSdNP6jwQEVq1LwdZcx4iwToedjH4GD8qIH8Uf3FmuV3DK1m5Wer8zNe2/wg+BXihLO/nqIftn5Hy96dx4OMGGKqyqJNjzz2DA9W8hkf9nAGbWs+WEbzZk1WVZnbaOKId0/9xm/Qo09UpqRJk5rXVDvQmH752Uc0d+ZEgsYxFCnOpKZl224mMbae8/tvZ2kSm1Ts3rndmmUcv1ynAdV4+TVjf1DfLnQb/2vTsSelY8KkZOeOrTRicC9qzP2Ptuty6MBeGtyva1gDI/38UPeL3FOM+g259Xyu//JTmjpxBL1StxH977katu/03Tu30dgR/QK2CwPZF158xbYJwwa8Sdt+2GwM2jFYqPbSq37XGTO8L3337dd+50fjmwBi9NIrr9MTT1XhZyeZ3zWRgOcH/bGF392hDPQj9Ty24uev0uPPGG3q2aWlQaxTp07DmNWhKs/XIPzedYGCYuXyRfQeKyTQZiF2OjqR2xdi54BlfBC71xu1omervuTQIv/kQwf30ZudnDUT2XPkpL6DxjmOTP1rJK6vOR06uN8uyy/Ny8QOjW1WvwZrRbJRj74jCC8bJ8HLplv7JnT8+FHbItHG0faiYSQGIiRbNm+kqROGUafuA6ho8ZK2tToRO2g+6zVowR/QF23P0xNv3rxB82dPpY9Xv68n++yDfGG6tEy5+33SAx307NLKljCCaDz9vxcCneqY9/W6z2jSWH8iHC0cc+XOQ+079zE0446N+i/jFGsqRw3t7fgsohhwfIkJGT72wQgtPp4rli00tLLWa+vE7tOPVtEjjz5lO/g7d/ZXR/IY6oyB9drhHOvEDu+848eOGG0NVMcZHhgMHdDdcdYlELED6f/5px+pQ9e+BI22ncQVscM7dvDIqcZg1a4d1rRQzBAi+TzqxG7siP70065t1Kv/KMqdt4C1aT7H0yePoi/WrhFi54NK5A4SDbHr1X8kTyMUC4icrtELhdgNH/M2ZbVMgel14GK1X3wy4DVVJrQpbTv1UofGFtNFZ8+cIkxFpk6VxpiatY7YnBwW8MIfOX4m5cyV16fOC6y6P8+q+yScn4nV+ekz+DrDYJTfummdkLSMkSB2GI3qjiCqsTqOe3bvYI1dR5UVcKs0dig0c9o4qvNaE5PUXblymV/Yu3ha+waVKlPetClE2Y0bvmKt0wDs+khc4OhzwVgclCpdjjoycVOiYwfN1xmewi9eoozKNraYdjvGUycXL16gbzd8aeso06RFB562vaX1VCfjmfydp3czZrqTMEWmCzTHnds2dPyYPlPlBWrUzFcLjg/ROW4fpr4wxYXnQTdvcCJ29Ru3pieefs68vH7PSAyk9dr49Rc0bdJI81y1Ew0ccS/jpsznqf/M6jLGFu374+oVw8zCJ4MPftmzk/r0aG9NNo9BtBs0ecM8VjuYgkydJq2fdgn5g/p0pp0/blNFja1O7FQGNN3QsiRJkkQlmdur/PvBVJs+hYlp305tGpllorGjEzu9fpBWmKZAMBth1ShD2ziob1f9FHMfNsrP8UyGEv352bZ1s2EmgGdcF7w/TvLgD1OeK5ctMLRTej72I/lNQH3tu/SmBx56FLumoB2/8W+QX9L8/s5k9Ifqr2DELtLPo07soIkry4M2Rerwnv1p1w7WnF6iIjwDpc8UwOyiRcNaQuzMXo3sTqIhdqHA1rFbP8IUDyQUYmdXZ3VW29eu19jMCnUqts/A0axRKWWet2LpQlr1/mL644+rZlr69OnZlqgh4QOpZNPG9ca0gzpWW0zT9BowWh2yXcMpg7js3xczXQbSUoy1OCCU+ssaNnawuQkmkSB2Ttfo0WcYlSxd3siOzVSstd5PPlpJSxbMNBwFkAfP4mFMzJXDCDQlHd5oYD2N4gJHv4u6THj4kSeMKTW7ajDlvO7zj2k3v3B1OyJrWdgjjpow25w2xIt4+sSRtH3b98bUP6YT87JNFzQf95Ysa57uRJBRoPeAUWwTVdosu3D+2/TpmpXGtLlKxDNZuEhRKn9/ReODBs1IKHaqtes2puo1XzWqCfZxU9cKto0Ejs+9UJNea9jSvBRIw9LFcwybKGiKYYdUslQ5atGmm2Ebqgo6mUSAgMD7V/1e8fGcxv2yY/v3bH92wegv2JnWqtOQ4FCi5CBPk/fs2tpnwGYldqdOHqeBvTsZpK43vzuUHS/qwHsGnvJoc9tOb7FZwyNG1ZHCWrXTbmtH7E6eOEojh/TyGUQ88FAlgle6Pv0X6gxE05Yd6clnYgYKqh0YyHy8ejlt/nY94d0JMhmuxPabgPuYMf99837wLZg8bgj9sOU7H/MblCtZuiyVr1CRSpQqawzMndoY6edRJ3b6NX/atZ2mc5QERbzxnsXvvwDbaSpp0bAmPfecP+Yq381WvGIl3In5/MQnsZuz6EOTZOAl3IOnoOwEH77xU98xpwhg39Su5S07Gb3889VqGdNoKm34wB6GzZI61rewnWnZJmZki2kqTFcFk4RC7EAgVi1f7Hc7TVvxy/w/rQ9e4K+/UsXvxR0XOPo1zGWCHSH5lTUbs6aPYwKwNaTa23XuTQ8+fEtTAGzat6xnGq3rFcD2cMS4maZdFj58bZrVoXPnzurFjP05Cz9gx4vUxj5e/P17dfIrE9uEuCJ24eCIe53Av1Vls3bu7BkOS9SUrly54nebsEeCFlLJ3p93s41vW3VobmF/Cy20kvmzp9DqVUvVoblNkyYNDR093XxPIGMwa69+ZC2WEiux07WjLd7oQo89+T9VlJq8Vs1s90MVHzfIncqELSs0fdESO2LXsvHLtk4S1ncZbPImjx8WtGl2xG7Xjz+w1n9syI5bTheJLbGDo9eQUdPMapcunktL2eY0thKN59GO2H3/3Tc0bmR/H0cKtLlipSfojQ49zeb3ebMNlS8XMyg0MyKwI8ROiJ35GMUXsQNZW7hsrWkvE0xbOGbSXMqe45bTBj407Vq9bt6D2qnJ9jc1a9dXhzwl0YV27vjBPNZ3rERg4pjBtGH953oR2/2EQOzg7QmvTzuB8bTyBkZ+3ZqVjWlavWxc4KhfLxL71v4ESZ8+aZTp3RjsGpjWmvXOKvN5XPfFJzRlwnDH0+AM0bBpGzPf6VmbtWCVOS0O78ZWTV4xNEDmiS524oLYhYtj+QoPUuc3B5p3NXxQT9a2bDKPrTuDR0w2NRrQzjSq+4K1CA0dNZXysWcmxE4Lp5+gh4BBupUEWomdbjYC8ggSqUTPK1GyDPXsFzOVDVtWOwcXda7brZXYbdn8LWvr3rKtFhrNabOXmgMIOJh17dDUtqyeaCV2E8cM4nfgF3qRWO/Hltjly1/QIOfqwnDWgAY7thKN59FK7DBgw/Q3NLtWKXR3ERo4PMZpCQONEvcWtRaLyLEQOyF25oMUX8QODRjJWg+EHYFA6zFj6hjDuFRX/cO+7rlqNenV12JeVNvYc3AYa+OsgmmJ9l36mMnwmMLUhQoVoDIwJdm911CTKCJdeTepMk7bhEDsAk2FW22V7IhdXODohG9s063Erm2Luj4hBoLVa9UUDBvIXoI8hegk1vJOdp86KUFdsIGaN2tSSNP+TtdW6XFB7MLFUdfC4XfckInaNbaXdRI4qkBDrETXkqm0We+sNOzocPzBindpwdxbHs8qX99aCTocW+bMmGgW0YkdbDHbtKhn5j3Kno7wuIWgzQ1erWrm3VP0Xuo7eJx5HNfEDs4N8Gx2ki49BhKiC0BgF9j4tepORc10ndj9zuFUMOiIlMSW2MHhC4MhJXiG1n78AU/lzw4pHJU6T22j8TxaiV3T16s7eiPDRGD42BmqOYYGWYidCUdEd8TGToMzPomdnaE6HB2OHD7ExtApjLhb+ThWlZrWUc2GBx1U31ZB7KUJHPpDGdUiHy+GA/t+ZsPya6wmZ/settnD6B8aQyWIf9Wtw62YdirNaet1Yvcl25JN49AIThIKsYsLHJ3aF9t0t8TOSmZhi3fk0H7H5qRNl8FnYAD70MVsz2gVK94qH/aNMOyHRx1CVcQm+KsXiV19dnCo8p9HMRwbMH0YSODlC29fJdYBFoIgz5i/UmWTE4E2C/DO9DnLTAcp6yBQJ3bo3268eo0SLxO7YNq0Zq06sWPNs+pWjLBHwaaKdWKHKfM32JwgUhJbYofr698k1R44KUFbu4t/M7v5NwOHMDhDBZNIP4+4nk7sgpFoIXbBeihy+ULsNCz1H1Gw6VDtNJ/d2P6I8dIeMHQiZf8vEKdPpQ4HiHsFt3Fdq6cXtfNC1POt+/hh9u/VkcnkQWuW7bHXid1HHy7nuGCTbNuORCvRsNPYoVy0ccQ1Iiluid0LHGNO1wqH27bPPvnQ0Dhbz4PDRa06DYwpPn0woZfDRwteoXAcCqQl1M/BvheJna45cnLO0e8DS/rB+F/JkP7daMe2LerQCCit21whduBG9mgOJLDHRSBkyH4ODvtWt5gp84RK7Ky4WO8fzmt4Dytpz6YqyohfpVm3XiV2GHzDsUZ3hLG2HcGkEXdvycKZPs4k1nKRfh5Rv07sgikFhNhZeyR6x0LsNGzjk9ihGXdxYN1GzTtwnK8KWqv8d+Hh+sGKJfTZJx84kjp1FpwD4CQQSPAxhTfj+0vmhRXxP7EQO2AXTRwD9U1s8twSO+uUYLhtgAfy7OnjHU+DpzFisN1TrKSPRtl6wtqPV/FU7ZSQtBFeJHb9Bo81VvjAfZ0+dZzat4qxebXeK46thv9dObYiovkrsdq2BdNc4byJ0xeaMeisSwUmVGIH+zrY2TmJ/iygjJOjhX6+V4mdaiPCDj31zPPGiiVOgyJ4KL/DzjRr+btgJ5F+HnENIXZ2SMd/mhA7rQ/im9ihKbrHGZYY2rrlW8NOBPGjECj0NIckwFI+IGOhij5q//zT1XSDQySk4PhaqA9LTu1hg9czvCxUuGIldsFsX8KpPxLhTiKlsVPtjhaOqv5Ibd0SO2tIBESy3/fLTyE3D0vfOQV71ivBmpvFS5Tlv9JGaJusvDSUVUINgKt/zCMVgsMtjnoMMjiLNOfwDoEEwckRpFyJ1dsUgWVHjp+tsmkmr5ji9BFXhXSbvA9XvkfvzJmqsiihEjvEIMRshZPoJA1G/PVqVQ46ANbP8dJUrPUe4YVeileaKVXmPirBYYaspjkoj+XO9vy003qqT0y8SDyPuIAQOz+YPZEgxE7rBi8Qu85vDuB4RA8ZrRrA06KIN+ZGdM+qcAL9hnJNrEwwf8nHpo2e9cMRSh1OZbxG7KKJoxMGsU13S0hgeI5pGyWhekmr8rHdYs3g1xq28omLF2oAXJ3YYRCEj7lbcYsjprMxra0kkGE5yui2YZd5GbWm7G2qC+KVzV282vy9WZ0h9LLYh33opBnvmsnQokKbqiShEjuE/EDoDyeBJzI8QCGhkrSEQuz0e4bmDlO0r3OcRD1QPjTdCM5ulUg/j6hfiJ0VZW8cC7HT+iG+iR28oKaxsTMWZoY0a8BhBLQF3LWmhryrhy1w+sGHXJlNwcn84VBR9SNJHL1G7KKNow20sU5yS0hy5WLNEAcnVuIUU03lR3ILz++ps5aYmohQtW/WsDQIfhqbRdH1e3GLI6bP4BSlZArHU1vHcdXsBKt4jGJtHNY5huzfu4ft4fxXl5g6+z1zhQXY7XXh6Vp94XW9bmsYmqH9uxsBplWZhErsjh89TJ3bxQSBV/eDLXA0wp1wQFxIqM9uQiR2xg3yfxgQDRsT420K21R4slslGs+jEDsryt44FmKn9UN8EzvdEw3NQtwieLDi7y+2n7jEyz9hibFjRw/xeolHg04voI4J0xb4BCmFsTU+ltc5oOi1P/80lqaB/Q/WYLQLnIo6Akn/IeOo8D33mkXe6trKiNBuJvy3A+eQv1mTAkPfUMRrxC7aOIaCSahl3BISaGInz3yXvaZjlptDwNFvv1nn2IQCBe82FmVfveo9R+cbxLFCQO1AXq/w4h41fpbpRATP8BaNAnuTolHWe0ZYj0Dr1jreiJZhrTPccCe6lhfVIjZd947NbEPPNG/dmR7nRd6VIG4g4gdapVP3/nTf/Q+byU5auxw5c9GQkdPMZd8Q77Jjm4Y+8cUSKrHDzTvFSqxcpRo1bBYT2Bne2fDSDiZeJHYgqblz5zV+TwgS7iRYhm/qrPfMbMRKRMxEq0TjeRRiZ0XZG8dC7LR+CIfYQbuWiX9QVqlWsy7Bu01JZ36Z6l6rsG/79fQple2zta5y4JNpOcAHb/Omb+iD9xfRWXbPtxNoP6bNWUpp06azy/ZJU96IWJgZwYn1NvsUtBxgzcUatWLiX8GzFtHaf2Q7QNSBtWof5Qj2lR57xlj6bOv3vkbPeCmlYSyt0qPvcHNtQRBa2I3ocpU/khd4zVur6GvFRsrGLi5wtN5HOMdwutGXUXq40pNUgxeJV4LR+xmHZ+4k22za9bWdswjsudZ9/pHhYIO1hhEkOwdr9x59vLK5Fu0AXpJq987t6tI+W9h74WP14/YfaNM3X7JjwEEeWJw1gtsivWDBwlT1xTo+zkNbNm804i/6VGRzYNVaIPYaAlNv5OtA6w17vrwcLqjCg5VoB3sQol6rRANH/Z2C6x3je/549Qra+8suXgLsPBW6uyiVYHspFRYFZbBcVue2jW3taLGMG1aU0A3oYQKxi2MCHtj/s6E9L1zkXsPjOyeTAiV2jhYJmdghBA+mG7d8t8FYJhD2vlg7+JW6jUxsMIBt3eRlvwErsMueIyfdxv+UwMEMzjxKOtksL4g8hBVxskeO9DcBfY1lDzEg+H7TBg5rtYG/HSf4fX/WiC2IwXIJXo4OnuY5cuZRTWfv2Nm0/L13zGN9J9LPoxA7HV3v7Aux0/pC1xIhntaA3p21XN/dQSMmUcFC9/gmhnBkDQSqn4IXDpbygXdcqIIXzdgR/clKmNT5CB7bq/8oM6ipSg+0xb0PHfCmETsvUDnkYekirF2ZJgTyaF3/Ekvc4GOvx9oLdj2V/xXHqJtqE6MuGsQO14w2juq+wt1aDerDPb9+7WcNDa71PDyLQ0ZOMVc5sOY7HQcjdlik3iogljpR0fOd4jTqZdT+m72HGkbl6thpixUksGyeLtHCEdPawzn4eDjP+OhhfXjQtkFvns9+m449DQ2lT2KAA8So696phR+BT8jETr9dzAJgYGAVJ+9sa7xA63mBjkGee3aJWfpNLxvpb4Iidvo11L7TbwaEF8oEJ/IZ6edRiJ3qEW9thdhp/aFPtwULbjts9DRDA6CdHtJuIGKHCvCBK1m6HAclTmloYVLwEjlp06WnrFlzGAvXI0gxjnXBi63jG/X9VpVQZbJlz2FozlKkSGHUiZdg5sxZKQt7IWLkmidvAb8P60ccpX6uFqVe1WW3xRRys9adeAHyZHbZZpqV2Fkjq5sFQ9iJa2KHJkUbxxBu26+IVVPlVyBIghOxw2nQYLXgNYSLlygTpJZb2dCsYhH5o2wDZSe6h6ZdvjUNWgdoH0IVtHcQk1F9CtnuXDtiF00cKzxQkTDVGmzwc4W13VMnDAsYygP3A01Ns9ZdCPUGE5hzTJ80wtBqWcsmVGIHu7nC9xS33o7P8aaN6w3ybhe4F9pRBOuNjQQidpH+JgQidnZthwfw2BH9bAPW6+Uj+TwKsdOR9c6+ELv/+gIhF6DZUrJo/gxauXyROvTbWm3L/Ao4JFiDhDoUc0yG/VP5Cg/zGqdNzcCjKAwNBD5YsREQlipVaxLsU5TAFqpdy5jpPJXutIVGCx8vTHdZtS8Iq7Jh3ee05sOlPs4gIJgz5q0wnUWc6rZLR9+gj6wyasIsg8QiPZyp2CuXLxleiHbTktZrOB1HAkenup3Scc0xk+b5Ye5UXk/HtHmzBi/52F3p+dhHXyJ+FkJxwPPOqnmC4f6BfXt4qaNVhI+pkyE/6sIg4v4HH6X7ebm7/LziiRJMmWGNTwjwx+LrCLFiF7JBneO0Bel5+dXGxsoD+K1Y5TcO74MYkFb7u2jjmDnzXWz71Y6KFithaM+hWVHT5zCr2PvLHpo3cyKHHzprbbLjMTRP6Jds3C/oJ1Un+uA42+F+zmYV6Bcn0ddKRiBkBP5VUpqnh7uzBhQSaEkxDCrh5IU+jJZY14rtxU4lOTiQe3U2e4E5AOzP0NfAAP0LB5X3Fs3x01Cq9sGWEe+q2IiT/RrqivQ3AfdTuEgxw3zgfjYhUI41uJbqa+yjv9fzPa9YtsDWfhNlrBKp5xErpeA5hIQboLhbhyZU8eEYe1FrG90cy1qxslYsL7mTgXr3H22u1Qp7s45sYxEsWrmbB8/tuU9XrkqNW8TYncGeCAvex1bwEpnC3ogZM96yG8Tor27NZ8KuDmQNBr9JkiTlF+2tcAO/83JKbghT2I2IxxMihWM83oLjpfHxxAcV2mR4biNQNtYejk3fpmSvxYyZMlGGDJnYhuiK8dzBVhQ2d3ZaFsdGOWSgH6DBAxkFaYSd3fnfzznaozpUE5VktCtlqlSseUtPJ44fNWwM3VwI95crdz7+2F/j31wSrvMYk/Ubbqr01Ll2xG4few2jjxH7MEOGjAbROc82t3Z2t566GReNwaAF7+cMbJd8mbXjMGvAoBnv13DimlqbEOnn0Vp/oOO6desFyo51nhC7RE7sSpQqS42atjW98PAkrf9qLU0ed2u0GusnK4onYtqodbselIeNa5UEi8auytltoTnAyF83PD7MXrKwyxEJHQHBMXSspKQgECoCTsQu1POlnHcREGIXnb5JVFOxWKsSxv7wICpavBSVu+8BKlS4mA+yWKwbITugiYhPAUmAJ19a/kvPWg1MOeTkUXmx4iUpd578Pk3DVE4nNpi9evWqT7p+gNEt7h0LtqdjGz2o9eGxmidfQSpVuryf8fHC+W/TquWL9SpknxEQHOUxEATiFgEhdnGLd1xeTYhddNBONMRuxLgZfoTICikCfg7u1zXOp2uwjFgjtlVIniw5JeXpLhBQEIhQBK7wMFg/eGCfT3F4NIIM3qovmZ9tlE9hywE0lgimGpspNktVCf5QcEzwXSg3kMAREGKXwDswQPOF2AUAx0VWoiF2cxevMQ20rXjBpgfG1CvYRi2Q1st6XqSOq1Z/herWbxZWdTBW/mTNCtaqLfSL04SKAt2v04Xg8bWEbfV2bN/qVCTRpQuOia7L5YY9hoAQO491SASbI8QugmBqVSVaYgdPql/27KQd276njV9/Ea9Tr6EQO0y3nmSN4pEjh4xgpFhDFt5qThKMkIDMwjkEhtZ7f95FO3ds4RUtjjhVl2jTBcdE2/Vy4x5BwLrEHUI7nTxx3COtk2a4QUCInRv0nM91JHZ/3bhOzQaW9zszZaqYZYb8Mj2cgKC/WEYLi2tfunTBcAuPpot+OFDAGaIgR6C/ceNvunnjprFFGIHLHILj8uWLdJE9oHAcjjzGqz1AbrIrPOoFkb169TIhrMclYMBegjLVGhxRwTE4RlJCEIg2ArA3hnkK3lmBlqSLdjuk/sgiIMQusniq2hyJ3T/8A2rUL2aJFXVCQiV2qv2yFQQEAUFAEBAEBIH4R0CIXXT6QIhddHCVWgUBQUAQEAQEAUFAEIhzBITYxTnkckFBQBAQBAQBQUAQEASig4AQu+jgKrUKAoKAICAICAKCgCAQ5wgIsYtzyOWCgoAgIAgIAoKAICAIRAcBR2L3982/qemAsn5XFecJP0gkQRAQBAQBQUAQEAQEAU8g4EjsLlw+R+1HPe7XSCF2fpBIgiAgCAgCgoAgIAgIAp5AQIidJ7pBGiEICAKCgCAgCAgCgoB7BITYucdQahAEBAFBQBAQBAQBQcATCAix80Q3SCMEAUFAEBAEBAFBQBBwj4AQO/cYSg2CgCAgCAgCgoAgIAh4AgEhdp7oBmmEICAICAKCgCAgCAgC7hEQYuceQ6lBEBAEBAFBQBAQBAQBTyAgxM4T3SCNEAQEAUFAEBAEBAFBwD0CQuzcYyg1CAKCgCAgCAgCgoAg4AkEhNh5ohukEYKAICAICAKCgCAgCLhHQIidewylBkFAEBAEBAFBQBAQBDyBgBA7T3SDNEIQEAQEAUFAEBAEBAH3CAixc4+h1CAICAKCgCAgCAgCgoAnEBBi54lukEYIAoKAICAICAKCgCDgHgEhdu4xlBoEAUFAEBAEBAFBQBDwBAJC7DzRDdIIQUAQEAQEAUFAEBAE3CMgxM49hlKDICAICAKCgCAgCAgCnkBAiJ0nukEaIQgIAoKAICAICAKCgHsEhNi5x1BqEAQEAUFAEBAEBAFBwBMICLHzRDdIIwQBQUAQEAQEAUFAEHCPgBA79xhKDYKAICAICAKCgCAgCHgCASF2nugGaYQgIAgIAoKAICAICALuERBi5x5DqUEQEAQEAUFAEBAEBAFPICDEzhPdII0QBAQBQUAQEAQEAUHAPQJC7NxjKDUIAoKAICAICAKCgCDgCQSE2HmiG6QRgoAgIAgIAoKAICAIuEdAiJ17DKUGQUAQEAQEAUFAEBAEPIGAEDtPdIM0QhAQBAQBQUAQEAQEAfcICLFzj6HUIAgIAoKAICAICAKCgCcQEGLniW6QRggCgoAgIAgIAoKAIOAeASF27jGUGgQBQUAQEAQEAUFAEPAEAkLsPNEN0ghBQBAQBAQBQUAQEATcIyDEzj2GUoMgIAgIAoKAICAICAKeQECInSe6QRohCAgCgoAgIAgIAoKAewQcid1fN65Ts4Hl/a6QMlVGvzRJEAQEAUFAEBAEBAFBQBCIfwT+D/zF7ZhlIKO3AAAAAElFTkSuQmCC\"}}]}],\"systemInstruction\":{\"parts\":[{\"text\":\"Read images carefully. Reply only with the visible text.\"}]},\"generationConfig\":{\"maxOutputTokens\":160,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream" + }, + "body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"jiggling restroom prison\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 291,\"candidatesTokenCount\": 5,\"totalTokenCount\": 402,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 33},{\"modality\": \"IMAGE\",\"tokenCount\": 258}],\"thoughtsTokenCount\": 106,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": \"p9wMarTSBZy3_uMPvM_bGA\"}\r\n\r\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json b/packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json index 25b561197c3..2110a6a99ba 100644 --- a/packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +++ b/packages/llm/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json @@ -14,7 +14,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"gpt-4.1-mini\",\"input\":[{\"role\":\"system\",\"content\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Say hi.\"}]}],\"prompt_cache_key\":\"recorded-cache-test\",\"max_output_tokens\":16,\"temperature\":0,\"stream\":true}" + "body": "{\"model\":\"gpt-4.1-mini\",\"input\":[{\"role\":\"system\",\"content\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Say hi.\"}]}],\"prompt_cache_key\":\"recorded-cache-test\",\"max_output_tokens\":16,\"temperature\":0,\"stream\":true,\"store\":false}" }, "response": { "status": 200, @@ -32,7 +32,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"model\":\"gpt-4.1-mini\",\"input\":[{\"role\":\"system\",\"content\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Say hi.\"}]}],\"prompt_cache_key\":\"recorded-cache-test\",\"max_output_tokens\":16,\"temperature\":0,\"stream\":true}" + "body": "{\"model\":\"gpt-4.1-mini\",\"input\":[{\"role\":\"system\",\"content\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Say hi.\"}]}],\"prompt_cache_key\":\"recorded-cache-test\",\"max_output_tokens\":16,\"temperature\":0,\"stream\":true,\"store\":false}" }, "response": { "status": 200, diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json new file mode 100644 index 00000000000..21fb8944f4c --- /dev/null +++ b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json @@ -0,0 +1,42 @@ +{ + "version": 1, + "metadata": { + "name": "openai-responses/openai-responses-gpt-5-5-image-tool-result", + "recordedAt": "2026-05-23T23:19:19.231Z", + "provider": "openai", + "route": "openai-responses", + "transport": "http", + "model": "gpt-5.5", + "tags": [ + "prefix:openai-responses", + "provider:openai", + "flagship", + "media", + "image", + "vision", + "tool", + "tool-result", + "golden" + ] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"system\",\"content\":\"Read images carefully. Reply only with the visible text, lowercase, no punctuation.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Use the read_screenshot tool, then reply with the words shown.\"}]},{\"type\":\"function_call\",\"call_id\":\"call_screenshot_1\",\"name\":\"read_screenshot\",\"arguments\":\"{}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_screenshot_1\",\"output\":[{\"type\":\"input_text\",\"text\":\"Image read successfully\"},{\"type\":\"input_image\",\"image_url\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnYAAACKCAYAAAAnmweyAAACKWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjYzMCIKICAgZXhpZjpVc2VyQ29tbWVudD0iU2NyZWVuc2hvdCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjEzOCIKICAgdGlmZjpZUmVzb2x1dGlvbj0iMTQ0LzEiCiAgIHRpZmY6WFJlc29sdXRpb249IjE0NC8xIgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+at0SpgAACrhpQ0NQSUNDIFByb2ZpbGUAAEiJlZcHUFNZF8fvey+dhJYQASmh994CSAmhBVCQDjZCEiAQQkxBwa4sruBaUBHBsqKrIgo2qg0RxbYo9r4gi4iyLhZsqHwPGMLufvN933xn5s75zXnn/u+5d959cx4AFFOuRCKC1QHIFsul0SEBjMSkZAb+JcACTUACnoDK5ckkrKioCIDahP+7fbgLoFF/y25U69+f/1fT4AtkPACgKJRT+TJeNsonAIABTyKVA4CgDEwWyCWjfB9lmhQtEOWBUU4fY8yoDi11nGljObHRbJQtASCQuVxpOgBkVzTOyOWlozrkWJQdxXyhGOUClH2zs3P4KLehbInmSFAe1Wem/kUn/W+aqUpNLjddyeN7GTNCoFAmEXHz/s/j+N+WLVJMrGGBDnKGNDQa9Xrouf2elROuZHHqjMgJFvLH8sc4QxEaN8E8GTt5gmWiGM4E87mB4Uod0YyICU4TBitzhHJO7AQLZEExEyzNiVaumyZlsyaYK52sQZEVp4xnCDhK/fyM2IQJzhXGz1DWlhUTPpnDVsalimjlXgTikIDJdYOV55At+8vehRzlXHlGbKjyHLiT9QvErElNWaKyNr4gMGgyJ06ZL5EHKNeSiKKU+QJRiDIuy41RzpWjL+fk3CjlGWZyw6ImGMQAOVAAPhCCHMAAgaiXAQkQAS7IkwsWykc3xM6R5EmF6RlyBgu9dQIGR8yzt2U4Ozq7AzB6h8dfkXf0sbsJ0a9MxlZVAeDTNDIycnIyFnYDgKMpAJDqJmOWcwBQ7wPg0imeQpo7Hhu7a1j0y6AGaEAHGAATYAnsgDNwB97AHwSBMBAJYkESmAt4IANkAylYABaDFaAQFIMNYAsoB7vAHnAAHAbHQAM4Bc6Bi+AquAHugEegC/SCV2AQfADDEAThIQpEhXQgQ8gMsoGcISbkCwVBEVA0lASlQOmQGFJAi6FVUDFUApVDu6Eq6CjUBJ2DLkOd0AOoG+qH3kJfYAQmwzRYHzaHHWAmzILD4Vh4DpwOz4fz4QJ4HVwGV8KH4Hr4HHwVvgN3wa/gIQQgKggdMULsECbCRiKRZCQNkSJLkSKkFKlEapBmpB25hXQhA8hnDA5DxTAwdhhvTCgmDsPDzMcsxazFlGMOYOoxbZhbmG7MIOY7loLVw9pgvbAcbCI2HbsAW4gtxe7D1mEvYO9ge7EfcDgcHWeB88CF4pJwmbhFuLW4HbhaXAuuE9eDG8Lj8Tp4G7wPPhLPxcvxhfht+EP4s/ib+F78J4IKwZDgTAgmJBPEhJWEUsJBwhnCTUIfYZioTjQjehEjiXxiHnE9cS+xmXid2EscJmmQLEg+pFhSJmkFqYxUQ7pAekx6p6KiYqziqTJTRaiyXKVM5YjKJZVulc9kTbI1mU2eTVaQ15H3k1vID8jvKBSKOcWfkkyRU9ZRqijnKU8pn1SpqvaqHFW+6jLVCtV61Zuqr9WIamZqLLW5avlqpWrH1a6rDagT1c3V2epc9aXqFepN6vfUhzSoGk4akRrZGms1Dmpc1nihidc01wzS5GsWaO7RPK/ZQ0WoJlQ2lUddRd1LvUDtpeFoFjQOLZNWTDtM66ANamlquWrFay3UqtA6rdVFR+jmdA5dRF9PP0a/S/8yRX8Ka4pgypopNVNuTvmoPVXbX1ugXaRdq31H+4sOQydIJ0tno06DzhNdjK617kzdBbo7dS/oDkylTfWeyptaNPXY1Id6sJ61XrTeIr09etf0hvQN9EP0Jfrb9M/rDxjQDfwNMg02G5wx6DekGvoaCg03G541fMnQYrAYIkYZo40xaKRnFGqkMNpt1GE0bGxhHGe80rjW+IkJyYRpkmay2aTVZNDU0HS66WLTatOHZkQzplmG2VazdrOP5hbmCearzRvMX1hoW3As8i2qLR5bUiz9LOdbVlretsJZMa2yrHZY3bCGrd2sM6wrrK/bwDbuNkKbHTadtlhbT1uxbaXtPTuyHcsu167artuebh9hv9K+wf61g6lDssNGh3aH745ujiLHvY6PnDSdwpxWOjU7vXW2duY5VzjfdqG4BLssc2l0eeNq4ypw3el6343qNt1ttVur2zd3D3epe417v4epR4rHdo97TBozirmWeckT6xnguczzlOdnL3cvudcxrz+97byzvA96v5hmMU0wbe+0Hh9jH67Pbp8uX4Zviu/Pvl1+Rn5cv0q/Z/4m/nz/ff59LCtWJusQ63WAY4A0oC7gI9uLvYTdEogEhgQWBXYEaQbFBZUHPQ02Dk4Prg4eDHELWRTSEooNDQ/dGHqPo8/hcao4g2EeYUvC2sLJ4THh5eHPIqwjpBHN0+HpYdM3TX88w2yGeEZDJIjkRG6KfBJlETU/6uRM3MyomRUzn0c7RS+Obo+hxsyLORjzITYgdn3sozjLOEVca7xa/Oz4qviPCYEJJQldiQ6JSxKvJukmCZMak/HJ8cn7kodmBc3aMqt3ttvswtl351jMWTjn8lzduaK5p+epzePOO56CTUlIOZjylRvJreQOpXJSt6cO8ti8rbxXfH/+Zn6/wEdQIuhL80krSXuR7pO+Kb0/wy+jNGNAyBaWC99khmbuyvyYFZm1P2tElCCqzSZkp2Q3iTXFWeK2HIOchTmdEhtJoaRrvtf8LfMHpeHSfTJINkfWKKehzdI1haXiB0V3rm9uRe6nBfELji/UWCheeC3POm9NXl9+cP4vizCLeItaFxstXrG4ewlrye6l0NLUpa3LTJYVLOtdHrL8wArSiqwVv650XFmy8v2qhFXNBfoFywt6fgj5obpQtVBaeG+19+pdP2J+FP7YscZlzbY134v4RVeKHYtLi7+u5a298pPTT2U/jaxLW9ex3n39zg24DeINdzf6bTxQolGSX9Kzafqm+s2MzUWb32+Zt+VyqWvprq2krYqtXWURZY3bTLdt2Pa1PKP8TkVARe12ve1rtn/cwd9xc6f/zppd+ruKd335Wfjz/d0hu+srzStL9+D25O55vjd+b/svzF+q9unuK973bb94f9eB6ANtVR5VVQf1Dq6vhqsV1f2HZh+6cTjwcGONXc3uWnpt8RFwRHHk5dGUo3ePhR9rPc48XnPC7MT2OmpdUT1Un1c/2JDR0NWY1NjZFNbU2uzdXHfS/uT+U0anKk5rnV5/hnSm4MzI2fyzQy2SloFz6ed6Wue1PjqfeP5228y2jgvhFy5dDL54vp3VfvaSz6VTl70uN11hXmm46n61/prbtbpf3X6t63DvqL/ucb3xhueN5s5pnWdu+t08dyvw1sXbnNtX78y403k37u79e7Pvdd3n33/xQPTgzcPch8OPlj/GPi56ov6k9Kne08rfrH6r7XLvOt0d2H3tWcyzRz28nle/y37/2lvwnPK8tM+wr+qF84tT/cH9N17Oetn7SvJqeKDwD40/tr+2fH3iT/8/rw0mDva+kb4Zebv2nc67/e9d37cORQ09/ZD9Yfhj0SedTwc+Mz+3f0n40je84Cv+a9k3q2/N38O/Px7JHhmRcKXcsVYAQQeclgbA2/0AUJIAoKI9BGnWeI89ZtD4f8EYgf/E4334mKGdSw3qRtsjdgsAR9BhvhwANX8ARlujWH8Au7gox0Q/PNa7jxoO/Yup8UK0Vjk9ta0C/7Txvv4vdf/TA6Xq3/y/AOOhDyne6KAWAAAAimVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA5KGAAcAAAASAAAAeKACAAQAAAABAAACdqADAAQAAAABAAAAigAAAABBU0NJSQAAAFNjAAAAAAAAAADxh4F4AAAAHGlET1QAAAACAAAAAAAAAEUAAAAoAAAARQAAAEUAAAbT33OL9AAABp9JREFUeAHs3F9olWUcB/DnLHCT/rgKQxbhtLwpkIqsLrxZQfQXKggEA/tjZuCFCRHR1Wg3XiyhoKgVeKFd1k1CFNGNRAhhkFAQFBlSkLhjbqtNbW3jeOB0dt6dHc905/d8dnXe5332nvf3+b7jfGXMUt+NG6aTLwIECBAgQIAAgY4XKCl2HZ+hAQgQIECAAAECcwKKnQeBAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEQLELEqQxCBAgQIAAAQKKnWeAAAECBAgQIBBEoPT+4NHp+WY58edPaeST1+c7ZY0AAQIECBAgQGAZCpQ+H/ln3mJXPnMy7R4eWIa37JYIECBAgAABAgTmE1Ds5lOxRoAAAQIECBDoQIFFF7uelb0dOKZbJkCAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATAcUuk6CNSYAAAQIECMQXUOziZ2xCAgQIECBAIBMBxS6ToI1JgAABAgQIxBdQ7OJnbEICBAgQIEAgEwHFLpOgjUmAAAECBAjEF1Ds4mdsQgIECBAgQCATgYbFburcZNoxdFcdQ8/K3ro1CwQIECBAgAABApdfoGGx+3d6Oj03uLHuDhW7OhILBAgQIECAAIFlIaDYLYsY3AQBAgQIEMhLYOvWp5dk4IMHDyzJdTvloopdpyTlPgkQIECAQCABxW5pwlTslsbVVQkQIECAAIECAcWuAOciTil2F4HnWwkQIECAAIHWBBS71twW+i7FbiEh5wkQIECAAIG2Cyh2bSedu2DDYnf2/Nn0wht31r2rv4qtI7FAgAABAgQILFJAsVskWJPbGxa78pmTaffwQN1lFLs6EgsECBAgQIDAIgUUu0WCNbldsatA3XrbxnTfA4/VsH343r7098REzVrRQXd3d3p+58upq+uK6rYD+99N5dFT1WMvFhboX3dzevTxLdWN4+Nn0v6Rt9P0zP+t6IsAAQKdIuAzoTgpxa7Yp9Wzil1FbuD+B9OLu16pcdzxzJPpr9Ona9aKDtb2r0t7931Qs2Xv0Gvp6LdHatYcFAs89MgTadv2XTWbtm15OE1OTtasOSBAgMByFvCZUJyOYlfs0+pZxa4ip9g1foRW9fam/nW3NN4wc+b8uXPp2PffFe5p9qRi16yUfQQILGcBxa44HcWu2KfVs4pdRU6xa/wI3X3v5rTn1cHGGypn3hoeSl8f/mrBfQttUOwWEnKeAIFOEFDsilNS7Ip9Wj2r2FXk/l/spqYm085nn0oTE+NN20b9IW622I2882b68otDTXs12qjYNZKxXiSwevUNqdTVlaZmfmVfLo8WbXWOwCURiPqZ0C48xa5dkrXXUewqHrO/buzru6mqMzrzBw9//H6ietzMi+6enrR+/Yaarb8d/yWNjY3VrLXjYLb8XHnV1dVLfXbo44t6n7X969OmezZXr3f815/TkW8Ozx3ffsemtP2lPdVzF15cs2pVWrGi+8Jhalexu/a669OaNX3V686++PGHYx3xxxNFjjUDOWirQKlUSvs/+jTN/gyWy6fm/lHW1jdwMQItCFzKz4QWbu+yf4titzQR/AcAAP//YCg3bwAAJAVJREFU7V0HvNXE0x1p0osivYMgIB2xIXb5oyKIoCAovUuvgvTee5WOFBEQULBgAxERQUBAlN5BQOkKCvrNCW7e3tzklpd738v73gy/R5LdzWZzNjc5Ozsze9unb1/7l2zkwuVz1H7U4345KVNl9EuThLhFIGu27DRuyjvmRU8cP0LdOzanv//+20wLd6dVu+5U6bGnzdMmjxtK679aax7b7aRPn4Gmz11uZr09eTR9vna1eZwYd2KDY2LEKdL3nCVrNho/dYFR7fXr16l+7WcjfQmpTxAQBCKMQN269SJc463qFiyI+T5G5QIer/Q2IXYe7yGb5tWoVY9efrWhkXPz5k3q1a01HTywz6ZkaEkpUtzOBG0ZpUyZyjhh86YNNHpYn6AnC7HzhSi2OPrWErdHadOmpatXr9K//9qO7+K2MS6uVrb8A9S15yCjhvggdv9fcHTRBYnm1HTp0tHly5cTzf1G80aF2EUH3URD7PIXKES33XZbyCheu/YnnTxxPGD53HnyUvLkKQKWiS3hypkzN2VhzVzmzFkp0x13Egjc2TOn6fSpE9SmQw+6K2t247pLFs2h5UvmB2wDMlOlTk158uSnzHdlpTvvykKpUqWmSxfO06+nT1KuPPno1debGXVc5LQu7RrRpUuXgtYZCWKXNGkyypsvf8Br/fnnH3Tq5ImAZVRmnrz5KVmyZHSDtZdHjx5WycY2W/achOcAcsedd/G9n6Bf9uwK6yUdDRyNBrn4D23KzvdmJ+fOnaFLFy+aWbfffjs9VbkqFSlawsDirizZ6N0Fs+j9pbe0XWZBm50778zMfVWQUqZKRRkyZKKzZ0/T0SOH+Ln8NWximJKfv/wFClKWLPyM8zMJuXDhd7rIf6f4d3fixDGbFtgn4Z5q1m5AVau/bBbo0bmFuW/dOcbPhZ12Oy5wTJ48OeXKnZfy5i9EWfg3fP63c4zhAeNZ/fOPP6xNDek4Y6Y7KD/XlztfAUqaJClBg3/k8AE68+vpgP2C90omPhdy7OgRxuQvSp8hA5Up9wBdv3aNU/+lrd9vMtJRpmChIlSocFG6fOkC3bhxg3Zs3/JfOeRGT25PmZLwPoTg/fQbYwbB+6fYvaX4PZmDf/PJ6ejh/XRg/146//tvRn6g//AsZ8iYybbI8WNH6a+/rpt5KPt0lWpUoGBhfmbvprTp0lO/nu3pZ353WCWa3wT0c15+v+Hdf8cdmbmNf9H587/RxfO/0+FD+/n3c97anKDHbp/HLFmyGnjgQngX4LlQgmep0N1FCe3+959/jPbu2/uT8VyqMkLsFBKR3SYaYjd38RrCByBU2fvzLur9ZruAxcdOnkvZsucKWKZOjacCvlytJ5e770GqVqMOFb6nuDXL7/jo4YPUvVNz+od/NE6SkV9ez75Qi57mjzk+XMFkxOC3+GX+bbBiRn4kiB0+LlNmLgl4vT27f6R+b3UIWEZlTpy+iIlCFgIxb1DneSM5W/YcVLteE7r/wUp+5P7K5Us0Y+pY2rRxnarCdhtNHG0vGEZimbIVqFuvIbZnrHr/XVo4b7qR91DFx6hu/eZ0Z+YsPmU/WbOCZr89wSdNP6jwQEVq1LwdZcx4iwToedjH4GD8qIH8Uf3FmuV3DK1m5Wer8zNe2/wg+BXihLO/nqIftn5Hy96dx4OMGGKqyqJNjzz2DA9W8hkf9nAGbWs+WEbzZk1WVZnbaOKId0/9xm/Qo09UpqRJk5rXVDvQmH752Uc0d+ZEgsYxFCnOpKZl224mMbae8/tvZ2kSm1Ts3rndmmUcv1ynAdV4+TVjf1DfLnQb/2vTsSelY8KkZOeOrTRicC9qzP2Ptuty6MBeGtyva1gDI/38UPeL3FOM+g259Xyu//JTmjpxBL1StxH977katu/03Tu30dgR/QK2CwPZF158xbYJwwa8Sdt+2GwM2jFYqPbSq37XGTO8L3337dd+50fjmwBi9NIrr9MTT1XhZyeZ3zWRgOcH/bGF392hDPQj9Ty24uev0uPPGG3q2aWlQaxTp07DmNWhKs/XIPzedYGCYuXyRfQeKyTQZiF2OjqR2xdi54BlfBC71xu1omervuTQIv/kQwf30ZudnDUT2XPkpL6DxjmOTP1rJK6vOR06uN8uyy/Ny8QOjW1WvwZrRbJRj74jCC8bJ8HLplv7JnT8+FHbItHG0faiYSQGIiRbNm+kqROGUafuA6ho8ZK2tToRO2g+6zVowR/QF23P0xNv3rxB82dPpY9Xv68n++yDfGG6tEy5+33SAx307NLKljCCaDz9vxcCneqY9/W6z2jSWH8iHC0cc+XOQ+079zE0446N+i/jFGsqRw3t7fgsohhwfIkJGT72wQgtPp4rli00tLLWa+vE7tOPVtEjjz5lO/g7d/ZXR/IY6oyB9drhHOvEDu+848eOGG0NVMcZHhgMHdDdcdYlELED6f/5px+pQ9e+BI22ncQVscM7dvDIqcZg1a4d1rRQzBAi+TzqxG7siP70065t1Kv/KMqdt4C1aT7H0yePoi/WrhFi54NK5A4SDbHr1X8kTyMUC4icrtELhdgNH/M2ZbVMgel14GK1X3wy4DVVJrQpbTv1UofGFtNFZ8+cIkxFpk6VxpiatY7YnBwW8MIfOX4m5cyV16fOC6y6P8+q+yScn4nV+ekz+DrDYJTfummdkLSMkSB2GI3qjiCqsTqOe3bvYI1dR5UVcKs0dig0c9o4qvNaE5PUXblymV/Yu3ha+waVKlPetClE2Y0bvmKt0wDs+khc4OhzwVgclCpdjjoycVOiYwfN1xmewi9eoozKNraYdjvGUycXL16gbzd8aeso06RFB562vaX1VCfjmfydp3czZrqTMEWmCzTHnds2dPyYPlPlBWrUzFcLjg/ROW4fpr4wxYXnQTdvcCJ29Ru3pieefs68vH7PSAyk9dr49Rc0bdJI81y1Ew0ccS/jpsznqf/M6jLGFu374+oVw8zCJ4MPftmzk/r0aG9NNo9BtBs0ecM8VjuYgkydJq2fdgn5g/p0pp0/blNFja1O7FQGNN3QsiRJkkQlmdur/PvBVJs+hYlp305tGpllorGjEzu9fpBWmKZAMBth1ShD2ziob1f9FHMfNsrP8UyGEv352bZ1s2EmgGdcF7w/TvLgD1OeK5ctMLRTej72I/lNQH3tu/SmBx56FLumoB2/8W+QX9L8/s5k9Ifqr2DELtLPo07soIkry4M2Rerwnv1p1w7WnF6iIjwDpc8UwOyiRcNaQuzMXo3sTqIhdqHA1rFbP8IUDyQUYmdXZ3VW29eu19jMCnUqts/A0axRKWWet2LpQlr1/mL644+rZlr69OnZlqgh4QOpZNPG9ca0gzpWW0zT9BowWh2yXcMpg7js3xczXQbSUoy1OCCU+ssaNnawuQkmkSB2Ttfo0WcYlSxd3siOzVSstd5PPlpJSxbMNBwFkAfP4mFMzJXDCDQlHd5oYD2N4gJHv4u6THj4kSeMKTW7ajDlvO7zj2k3v3B1OyJrWdgjjpow25w2xIt4+sSRtH3b98bUP6YT87JNFzQf95Ysa57uRJBRoPeAUWwTVdosu3D+2/TpmpXGtLlKxDNZuEhRKn9/ReODBs1IKHaqtes2puo1XzWqCfZxU9cKto0Ejs+9UJNea9jSvBRIw9LFcwybKGiKYYdUslQ5atGmm2Ebqgo6mUSAgMD7V/1e8fGcxv2yY/v3bH92wegv2JnWqtOQ4FCi5CBPk/fs2tpnwGYldqdOHqeBvTsZpK43vzuUHS/qwHsGnvJoc9tOb7FZwyNG1ZHCWrXTbmtH7E6eOEojh/TyGUQ88FAlgle6Pv0X6gxE05Yd6clnYgYKqh0YyHy8ejlt/nY94d0JMhmuxPabgPuYMf99837wLZg8bgj9sOU7H/MblCtZuiyVr1CRSpQqawzMndoY6edRJ3b6NX/atZ2mc5QERbzxnsXvvwDbaSpp0bAmPfecP+Yq381WvGIl3In5/MQnsZuz6EOTZOAl3IOnoOwEH77xU98xpwhg39Su5S07Gb3889VqGdNoKm34wB6GzZI61rewnWnZJmZki2kqTFcFk4RC7EAgVi1f7Hc7TVvxy/w/rQ9e4K+/UsXvxR0XOPo1zGWCHSH5lTUbs6aPYwKwNaTa23XuTQ8+fEtTAGzat6xnGq3rFcD2cMS4maZdFj58bZrVoXPnzurFjP05Cz9gx4vUxj5e/P17dfIrE9uEuCJ24eCIe53Av1Vls3bu7BkOS9SUrly54nebsEeCFlLJ3p93s41vW3VobmF/Cy20kvmzp9DqVUvVoblNkyYNDR093XxPIGMwa69+ZC2WEiux07WjLd7oQo89+T9VlJq8Vs1s90MVHzfIncqELSs0fdESO2LXsvHLtk4S1ncZbPImjx8WtGl2xG7Xjz+w1n9syI5bTheJLbGDo9eQUdPMapcunktL2eY0thKN59GO2H3/3Tc0bmR/H0cKtLlipSfojQ49zeb3ebMNlS8XMyg0MyKwI8ROiJ35GMUXsQNZW7hsrWkvE0xbOGbSXMqe45bTBj407Vq9bt6D2qnJ9jc1a9dXhzwl0YV27vjBPNZ3rERg4pjBtGH953oR2/2EQOzg7QmvTzuB8bTyBkZ+3ZqVjWlavWxc4KhfLxL71v4ESZ8+aZTp3RjsGpjWmvXOKvN5XPfFJzRlwnDH0+AM0bBpGzPf6VmbtWCVOS0O78ZWTV4xNEDmiS524oLYhYtj+QoPUuc3B5p3NXxQT9a2bDKPrTuDR0w2NRrQzjSq+4K1CA0dNZXysWcmxE4Lp5+gh4BBupUEWomdbjYC8ggSqUTPK1GyDPXsFzOVDVtWOwcXda7brZXYbdn8LWvr3rKtFhrNabOXmgMIOJh17dDUtqyeaCV2E8cM4nfgF3qRWO/Hltjly1/QIOfqwnDWgAY7thKN59FK7DBgw/Q3NLtWKXR3ERo4PMZpCQONEvcWtRaLyLEQOyF25oMUX8QODRjJWg+EHYFA6zFj6hjDuFRX/cO+7rlqNenV12JeVNvYc3AYa+OsgmmJ9l36mMnwmMLUhQoVoDIwJdm911CTKCJdeTepMk7bhEDsAk2FW22V7IhdXODohG9s063Erm2Luj4hBoLVa9UUDBvIXoI8hegk1vJOdp86KUFdsIGaN2tSSNP+TtdW6XFB7MLFUdfC4XfckInaNbaXdRI4qkBDrETXkqm0We+sNOzocPzBindpwdxbHs8qX99aCTocW+bMmGgW0YkdbDHbtKhn5j3Kno7wuIWgzQ1erWrm3VP0Xuo7eJx5HNfEDs4N8Gx2ki49BhKiC0BgF9j4tepORc10ndj9zuFUMOiIlMSW2MHhC4MhJXiG1n78AU/lzw4pHJU6T22j8TxaiV3T16s7eiPDRGD42BmqOYYGWYidCUdEd8TGToMzPomdnaE6HB2OHD7ExtApjLhb+ThWlZrWUc2GBx1U31ZB7KUJHPpDGdUiHy+GA/t+ZsPya6wmZ/settnD6B8aQyWIf9Wtw62YdirNaet1Yvcl25JN49AIThIKsYsLHJ3aF9t0t8TOSmZhi3fk0H7H5qRNl8FnYAD70MVsz2gVK94qH/aNMOyHRx1CVcQm+KsXiV19dnCo8p9HMRwbMH0YSODlC29fJdYBFoIgz5i/UmWTE4E2C/DO9DnLTAcp6yBQJ3bo3268eo0SLxO7YNq0Zq06sWPNs+pWjLBHwaaKdWKHKfM32JwgUhJbYofr698k1R44KUFbu4t/M7v5NwOHMDhDBZNIP4+4nk7sgpFoIXbBeihy+ULsNCz1H1Gw6VDtNJ/d2P6I8dIeMHQiZf8vEKdPpQ4HiHsFt3Fdq6cXtfNC1POt+/hh9u/VkcnkQWuW7bHXid1HHy7nuGCTbNuORCvRsNPYoVy0ccQ1Iiluid0LHGNO1wqH27bPPvnQ0Dhbz4PDRa06DYwpPn0woZfDRwteoXAcCqQl1M/BvheJna45cnLO0e8DS/rB+F/JkP7daMe2LerQCCit21whduBG9mgOJLDHRSBkyH4ODvtWt5gp84RK7Ky4WO8fzmt4Dytpz6YqyohfpVm3XiV2GHzDsUZ3hLG2HcGkEXdvycKZPs4k1nKRfh5Rv07sgikFhNhZeyR6x0LsNGzjk9ihGXdxYN1GzTtwnK8KWqv8d+Hh+sGKJfTZJx84kjp1FpwD4CQQSPAxhTfj+0vmhRXxP7EQO2AXTRwD9U1s8twSO+uUYLhtgAfy7OnjHU+DpzFisN1TrKSPRtl6wtqPV/FU7ZSQtBFeJHb9Bo81VvjAfZ0+dZzat4qxebXeK46thv9dObYiovkrsdq2BdNc4byJ0xeaMeisSwUmVGIH+zrY2TmJ/iygjJOjhX6+V4mdaiPCDj31zPPGiiVOgyJ4KL/DzjRr+btgJ5F+HnENIXZ2SMd/mhA7rQ/im9ihKbrHGZYY2rrlW8NOBPGjECj0NIckwFI+IGOhij5q//zT1XSDQySk4PhaqA9LTu1hg9czvCxUuGIldsFsX8KpPxLhTiKlsVPtjhaOqv5Ibd0SO2tIBESy3/fLTyE3D0vfOQV71ivBmpvFS5Tlv9JGaJusvDSUVUINgKt/zCMVgsMtjnoMMjiLNOfwDoEEwckRpFyJ1dsUgWVHjp+tsmkmr5ji9BFXhXSbvA9XvkfvzJmqsiihEjvEIMRshZPoJA1G/PVqVQ46ANbP8dJUrPUe4YVeileaKVXmPirBYYaspjkoj+XO9vy003qqT0y8SDyPuIAQOz+YPZEgxE7rBi8Qu85vDuB4RA8ZrRrA06KIN+ZGdM+qcAL9hnJNrEwwf8nHpo2e9cMRSh1OZbxG7KKJoxMGsU13S0hgeI5pGyWhekmr8rHdYs3g1xq28omLF2oAXJ3YYRCEj7lbcYsjprMxra0kkGE5yui2YZd5GbWm7G2qC+KVzV282vy9WZ0h9LLYh33opBnvmsnQokKbqiShEjuE/EDoDyeBJzI8QCGhkrSEQuz0e4bmDlO0r3OcRD1QPjTdCM5ulUg/j6hfiJ0VZW8cC7HT+iG+iR28oKaxsTMWZoY0a8BhBLQF3LWmhryrhy1w+sGHXJlNwcn84VBR9SNJHL1G7KKNow20sU5yS0hy5WLNEAcnVuIUU03lR3ILz++ps5aYmohQtW/WsDQIfhqbRdH1e3GLI6bP4BSlZArHU1vHcdXsBKt4jGJtHNY5huzfu4ft4fxXl5g6+z1zhQXY7XXh6Vp94XW9bmsYmqH9uxsBplWZhErsjh89TJ3bxQSBV/eDLXA0wp1wQFxIqM9uQiR2xg3yfxgQDRsT420K21R4slslGs+jEDsryt44FmKn9UN8EzvdEw3NQtwieLDi7y+2n7jEyz9hibFjRw/xeolHg04voI4J0xb4BCmFsTU+ltc5oOi1P/80lqaB/Q/WYLQLnIo6Akn/IeOo8D33mkXe6trKiNBuJvy3A+eQv1mTAkPfUMRrxC7aOIaCSahl3BISaGInz3yXvaZjlptDwNFvv1nn2IQCBe82FmVfveo9R+cbxLFCQO1AXq/w4h41fpbpRATP8BaNAnuTolHWe0ZYj0Dr1jreiJZhrTPccCe6lhfVIjZd947NbEPPNG/dmR7nRd6VIG4g4gdapVP3/nTf/Q+byU5auxw5c9GQkdPMZd8Q77Jjm4Y+8cUSKrHDzTvFSqxcpRo1bBYT2Bne2fDSDiZeJHYgqblz5zV+TwgS7iRYhm/qrPfMbMRKRMxEq0TjeRRiZ0XZG8dC7LR+CIfYQbuWiX9QVqlWsy7Bu01JZ36Z6l6rsG/79fQple2zta5y4JNpOcAHb/Omb+iD9xfRWXbPtxNoP6bNWUpp06azy/ZJU96IWJgZwYn1NvsUtBxgzcUatWLiX8GzFtHaf2Q7QNSBtWof5Qj2lR57xlj6bOv3vkbPeCmlYSyt0qPvcHNtQRBa2I3ocpU/khd4zVur6GvFRsrGLi5wtN5HOMdwutGXUXq40pNUgxeJV4LR+xmHZ+4k22za9bWdswjsudZ9/pHhYIO1hhEkOwdr9x59vLK5Fu0AXpJq987t6tI+W9h74WP14/YfaNM3X7JjwEEeWJw1gtsivWDBwlT1xTo+zkNbNm804i/6VGRzYNVaIPYaAlNv5OtA6w17vrwcLqjCg5VoB3sQol6rRANH/Z2C6x3je/549Qra+8suXgLsPBW6uyiVYHspFRYFZbBcVue2jW3taLGMG1aU0A3oYQKxi2MCHtj/s6E9L1zkXsPjOyeTAiV2jhYJmdghBA+mG7d8t8FYJhD2vlg7+JW6jUxsMIBt3eRlvwErsMueIyfdxv+UwMEMzjxKOtksL4g8hBVxskeO9DcBfY1lDzEg+H7TBg5rtYG/HSf4fX/WiC2IwXIJXo4OnuY5cuZRTWfv2Nm0/L13zGN9J9LPoxA7HV3v7Aux0/pC1xIhntaA3p21XN/dQSMmUcFC9/gmhnBkDQSqn4IXDpbygXdcqIIXzdgR/clKmNT5CB7bq/8oM6ipSg+0xb0PHfCmETsvUDnkYekirF2ZJgTyaF3/Ekvc4GOvx9oLdj2V/xXHqJtqE6MuGsQO14w2juq+wt1aDerDPb9+7WcNDa71PDyLQ0ZOMVc5sOY7HQcjdlik3iogljpR0fOd4jTqZdT+m72HGkbl6thpixUksGyeLtHCEdPawzn4eDjP+OhhfXjQtkFvns9+m449DQ2lT2KAA8So696phR+BT8jETr9dzAJgYGAVJ+9sa7xA63mBjkGee3aJWfpNLxvpb4Iidvo11L7TbwaEF8oEJ/IZ6edRiJ3qEW9thdhp/aFPtwULbjts9DRDA6CdHtJuIGKHCvCBK1m6HAclTmloYVLwEjlp06WnrFlzGAvXI0gxjnXBi63jG/X9VpVQZbJlz2FozlKkSGHUiZdg5sxZKQt7IWLkmidvAb8P60ccpX6uFqVe1WW3xRRys9adeAHyZHbZZpqV2Fkjq5sFQ9iJa2KHJkUbxxBu26+IVVPlVyBIghOxw2nQYLXgNYSLlygTpJZb2dCsYhH5o2wDZSe6h6ZdvjUNWgdoH0IVtHcQk1F9CtnuXDtiF00cKzxQkTDVGmzwc4W13VMnDAsYygP3A01Ns9ZdCPUGE5hzTJ80wtBqWcsmVGIHu7nC9xS33o7P8aaN6w3ybhe4F9pRBOuNjQQidpH+JgQidnZthwfw2BH9bAPW6+Uj+TwKsdOR9c6+ELv/+gIhF6DZUrJo/gxauXyROvTbWm3L/Ao4JFiDhDoUc0yG/VP5Cg/zGqdNzcCjKAwNBD5YsREQlipVaxLsU5TAFqpdy5jpPJXutIVGCx8vTHdZtS8Iq7Jh3ee05sOlPs4gIJgz5q0wnUWc6rZLR9+gj6wyasIsg8QiPZyp2CuXLxleiHbTktZrOB1HAkenup3Scc0xk+b5Ye5UXk/HtHmzBi/52F3p+dhHXyJ+FkJxwPPOqnmC4f6BfXt4qaNVhI+pkyE/6sIg4v4HH6X7ebm7/LziiRJMmWGNTwjwx+LrCLFiF7JBneO0Bel5+dXGxsoD+K1Y5TcO74MYkFb7u2jjmDnzXWz71Y6KFithaM+hWVHT5zCr2PvLHpo3cyKHHzprbbLjMTRP6Jds3C/oJ1Un+uA42+F+zmYV6Bcn0ddKRiBkBP5VUpqnh7uzBhQSaEkxDCrh5IU+jJZY14rtxU4lOTiQe3U2e4E5AOzP0NfAAP0LB5X3Fs3x01Cq9sGWEe+q2IiT/RrqivQ3AfdTuEgxw3zgfjYhUI41uJbqa+yjv9fzPa9YtsDWfhNlrBKp5xErpeA5hIQboLhbhyZU8eEYe1FrG90cy1qxslYsL7mTgXr3H22u1Qp7s45sYxEsWrmbB8/tuU9XrkqNW8TYncGeCAvex1bwEpnC3ogZM96yG8Tor27NZ8KuDmQNBr9JkiTlF+2tcAO/83JKbghT2I2IxxMihWM83oLjpfHxxAcV2mR4biNQNtYejk3fpmSvxYyZMlGGDJnYhuiK8dzBVhQ2d3ZaFsdGOWSgH6DBAxkFaYSd3fnfzznaozpUE5VktCtlqlSseUtPJ44fNWwM3VwI95crdz7+2F/j31wSrvMYk/Ubbqr01Ll2xG4few2jjxH7MEOGjAbROc82t3Z2t566GReNwaAF7+cMbJd8mbXjMGvAoBnv13DimlqbEOnn0Vp/oOO6desFyo51nhC7RE7sSpQqS42atjW98PAkrf9qLU0ed2u0GusnK4onYtqodbselIeNa5UEi8auytltoTnAyF83PD7MXrKwyxEJHQHBMXSspKQgECoCTsQu1POlnHcREGIXnb5JVFOxWKsSxv7wICpavBSVu+8BKlS4mA+yWKwbITugiYhPAUmAJ19a/kvPWg1MOeTkUXmx4iUpd578Pk3DVE4nNpi9evWqT7p+gNEt7h0LtqdjGz2o9eGxmidfQSpVuryf8fHC+W/TquWL9SpknxEQHOUxEATiFgEhdnGLd1xeTYhddNBONMRuxLgZfoTICikCfg7u1zXOp2uwjFgjtlVIniw5JeXpLhBQEIhQBK7wMFg/eGCfT3F4NIIM3qovmZ9tlE9hywE0lgimGpspNktVCf5QcEzwXSg3kMAREGKXwDswQPOF2AUAx0VWoiF2cxevMQ20rXjBpgfG1CvYRi2Q1st6XqSOq1Z/herWbxZWdTBW/mTNCtaqLfSL04SKAt2v04Xg8bWEbfV2bN/qVCTRpQuOia7L5YY9hoAQO491SASbI8QugmBqVSVaYgdPql/27KQd276njV9/Ea9Tr6EQO0y3nmSN4pEjh4xgpFhDFt5qThKMkIDMwjkEhtZ7f95FO3ds4RUtjjhVl2jTBcdE2/Vy4x5BwLrEHUI7nTxx3COtk2a4QUCInRv0nM91JHZ/3bhOzQaW9zszZaqYZYb8Mj2cgKC/WEYLi2tfunTBcAuPpot+OFDAGaIgR6C/ceNvunnjprFFGIHLHILj8uWLdJE9oHAcjjzGqz1AbrIrPOoFkb169TIhrMclYMBegjLVGhxRwTE4RlJCEIg2ArA3hnkK3lmBlqSLdjuk/sgiIMQusniq2hyJ3T/8A2rUL2aJFXVCQiV2qv2yFQQEAUFAEBAEBIH4R0CIXXT6QIhddHCVWgUBQUAQEAQEAUFAEIhzBITYxTnkckFBQBAQBAQBQUAQEASig4AQu+jgKrUKAoKAICAICAKCgCAQ5wgIsYtzyOWCgoAgIAgIAoKAICAIRAcBR2L3982/qemAsn5XFecJP0gkQRAQBAQBQUAQEAQEAU8g4EjsLlw+R+1HPe7XSCF2fpBIgiAgCAgCgoAgIAgIAp5AQIidJ7pBGiEICAKCgCAgCAgCgoB7BITYucdQahAEBAFBQBAQBAQBQcATCAix80Q3SCMEAUFAEBAEBAFBQBBwj4AQO/cYSg2CgCAgCAgCgoAgIAh4AgEhdp7oBmmEICAICAKCgCAgCAgC7hEQYuceQ6lBEBAEBAFBQBAQBAQBTyAgxM4T3SCNEAQEAUFAEBAEBAFBwD0CQuzcYyg1CAKCgCAgCAgCgoAg4AkEhNh5ohukEYKAICAICAKCgCAgCLhHQIidewylBkFAEBAEBAFBQBAQBDyBgBA7T3SDNEIQEAQEAUFAEBAEBAH3CAixc4+h1CAICAKCgCAgCAgCgoAnEBBi54lukEYIAoKAICAICAKCgCDgHgEhdu4xlBoEAUFAEBAEBAFBQBDwBAJC7DzRDdIIQUAQEAQEAUFAEBAE3CMgxM49hlKDICAICAKCgCAgCAgCnkBAiJ0nukEaIQgIAoKAICAICAKCgHsEhNi5x1BqEAQEAUFAEBAEBAFBwBMICLHzRDdIIwQBQUAQEAQEAUFAEHCPgBA79xhKDYKAICAICAKCgCAgCHgCASF2nugGaYQgIAgIAoKAICAICALuERBi5x5DqUEQEAQEAUFAEBAEBAFPICDEzhPdII0QBAQBQUAQEAQEAUHAPQJC7NxjKDUIAoKAICAICAKCgCDgCQSE2HmiG6QRgoAgIAgIAoKAICAIuEdAiJ17DKUGQUAQEAQEAUFAEBAEPIGAEDtPdIM0QhAQBAQBQUAQEAQEAfcICLFzj6HUIAgIAoKAICAICAKCgCcQEGLniW6QRggCgoAgIAgIAoKAIOAeASF27jGUGgQBQUAQEAQEAUFAEPAEAkLsPNEN0ghBQBAQBAQBQUAQEATcIyDEzj2GUoMgIAgIAoKAICAICAKeQECInSe6QRohCAgCgoAgIAgIAoKAewQcid1fN65Ts4Hl/a6QMlVGvzRJEAQEAUFAEBAEBAFBQBCIfwT+D/zF7ZhlIKO3AAAAAElFTkSuQmCC\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"read_screenshot\",\"description\":\"Capture a screenshot of the current screen.\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_06646cef0abb6407016a1235f665a88197a401db20cfc8c787\",\"object\":\"response\",\"created_at\":1779578358,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Capture a screenshot of the current screen.\",\"name\":\"read_screenshot\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"required\":[]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_06646cef0abb6407016a1235f665a88197a401db20cfc8c787\",\"object\":\"response\",\"created_at\":1779578358,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Capture a screenshot of the current screen.\",\"name\":\"read_screenshot\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"required\":[]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"j\",\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"obfuscation\":\"eWkUz3qb2ZTKrat\",\"output_index\":0,\"sequence_number\":4}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"igg\",\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"obfuscation\":\"rPnd9lcMUqqob\",\"output_index\":0,\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ling\",\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"obfuscation\":\"IuJYWK4DiIvE\",\"output_index\":0,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" restroom\",\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"obfuscation\":\"v2MVmAR\",\"output_index\":0,\"sequence_number\":7}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" prison\",\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"obfuscation\":\"AfKH50yJZ\",\"output_index\":0,\"sequence_number\":8}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":9,\"text\":\"jiggling restroom prison\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"jiggling restroom prison\"},\"sequence_number\":10}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"jiggling restroom prison\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":11}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_06646cef0abb6407016a1235f665a88197a401db20cfc8c787\",\"object\":\"response\",\"created_at\":1779578358,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578359,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"msg_06646cef0abb6407016a1235f703708197bd125c4f32fb7b69\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"jiggling restroom prison\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"medium\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Capture a screenshot of the current screen.\",\"name\":\"read_screenshot\",\"parameters\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false,\"required\":[]},\"strict\":true}],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":227,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":9,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":236},\"user\":null,\"metadata\":{}},\"sequence_number\":12}\n\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json new file mode 100644 index 00000000000..850e381cafb --- /dev/null +++ b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json @@ -0,0 +1,58 @@ +{ + "version": 1, + "metadata": { + "name": "openai-responses/openai-responses-gpt-5-5-reasoning-continuation", + "recordedAt": "2026-05-23T23:19:06.776Z", + "provider": "openai", + "route": "openai-responses", + "transport": "http", + "model": "gpt-5.5", + "tags": [ + "prefix:openai-responses", + "provider:openai", + "flagship", + "reasoning", + "continuation", + "encrypted-reasoning", + "golden" + ] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"system\",\"content\":\"Show concise reasoning when the provider supports visible reasoning summaries.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":120,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXnglldg7hhpTBATVqj7sThK5ATieOVR8sZGYPDW2zYopwpKxA3RyRccK_FPjRvvlzrvL-FitOxmdMGBaKa5jncrT9hHo5IMhsFsCEHkQ1x5tlrKPqtfwJ_LFexR0h_IpPogu8wlVAkHRoWQoq61o9vBxjMOEsq6dtXu09959gXnAvJA3jN_mqNkRZ7Yp6LaJJtLDAAtt_dhX8veoEFXZ412lCY4zcaMvC5o0yq6MPvLIN4NhHmfPKkVAy-j8wGlgA42KR4wd5-VeFXUdeSn32dlNLZZxBFa9w6iTgCQ9aF-3C7RB4OXeSY782QUD1dRyFybd7vJtjlptwXBntSHZ9wugoKSDEj0KnvQKG_WiCWuJvkGiOVno4MAs5QnCmKBnpak5OV1wOhPwX2ez6OmAYT4mMKIogdfivVvUxMrmdVJzgE85WoZEAU2ZporxVXkI7_8p0L6dxxwk_IKiKSCz-bZgsCtOP5Jsr5GeI831nVv272kZ3DugV-hcjGHAE5T9KhebzpFjsdxnJcfxuGY8SyRaLlUAHM_37H4veHsOzyhCoaG8mMaT3gIb4tAvM7ezd1xzLsFae89P5xCv_fNeoV7qmf2IWDWUi1vitIib5w9jsclWRqYaLVZR0GK6dYyNJ1DXDOOcWRdH7UJakv1m2koUbcYWBuxao7sc-af_9ySKAloWhb6QjiVElJHYtwraJBtX-CLBVHEYqAXmZgMUWVbz8NNRA6JS1TrOys7_LiQtXXubLWas_66LyaqmB-628LCUitUISYYc2wmq1uUm7gjPA53Wm4F7VU6g-PO7bt1O0Nd-jasisPXINTX3Z4hgC1APPEq29iEHwmEPnicO_Nu6U3JLfq4DD6r1oLK-RnIp3Ratw0P-Gwog86RBLGUWIEIKdFu6m9d1TI8rIBbAVaBA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"3nRhhCWA1H8\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"obfuscation\":\"60NqChSEyXHKsoy\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e74e148195beb46e1925d20292\",\"object\":\"response\",\"created_at\":1779578343,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578344,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXoMO9Ci_Q06HKQ0YDBarUvbp9ulkR9W2RXWPbx7XKokNCrKUZX-pPPGpUg6r-vTXe8iEX-oED6TmjxZV_nyo838x6pmQJlDqz5JECs2axIrUbCjv9xBt3ob8eAyOizhKFjp3dJNu4i01c38MPZ5QYpD24uCKf69jzjUfydKIEjbo0VhP3K6SDG0V9ZUtua-e6WMqzIg-W5Zs3u64DxGw974ntmvNsx8lsuLR-bk9S5ZZ7zPlCG2Emwfph8UE5HJmIfmMxYlrY5qmXSWKDhse9hovQj-TrvbllP-0vLNQWEPLc3aUfVrWWR9i3NZZ-nxJZiIJPCF3xxIIyKaLh9a6Lh9J6Z-brsvVfbVJWXIGZhsu-uKk6Gwoqo56KqHdNaPF7lkPo5GAWfMrweCnJZ4o_j-oWm8BwTkXxrLib4XYKDO2JNqrNdbmy8rZ7UGgW_DVTiNyZi6LoRfSuvK45MWV2uzB_OJ9LBcqgscY4HyPvKrhGG4Peh4iXuBUCyQQ2IudM5GbeeMOAF3dnEzZff68SwE1H56CO6PtKhVQ6cFJMf7LwI5LFFio0qJnEDx-MejvU7PxmYW7R3MEbgjbsuEFU5KnRVYsgug3_Bq1vXdmP2qhebufFZwz26SwaFqyn3xjwCP8-GR7lWCZ2EvUvWtfxJ5_zgkZg06UsF4Eo_CWKFdp0ao43nemNJxOlMzFa6tPuCgplmD0oYoQ316f-bWK02-eJk56S7G4bZSk8cQfExfIZMjW2f-qrxvfxEpFiXsZF80BQwgRUOeKjsqidg2ihdldRkXGn3vX8p15mf1UgstU8y3DNd2_qJe1f_pEl6rWNXoxFdSRCTG7wTAqbCCmuDgCKGhNQY9tfJNsFqgWBIGkqKy88DN_HiWywJjJ-5u9aoe68yDK-E0TMDqs7ZrTely1wvmkl2yF0XQttaB30taxkIcRR-n0PRO-CRNA_9nJkw9ZsBb8oBjyqWH_mwSijT5g==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235e8d64c81959a41f8db3ea7b66c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":51},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]},{\"type\":\"reasoning\",\"id\":\"rs_0a0794dab3b8ec7d016a1235e7ce3881958a5eca32a36a14c5\",\"summary\":[],\"encrypted_content\":\"gAAAAABqEjXoGMCw3WDXpoD9151PEr2Lt8raW7KBKefQhZJGWx5f8jy152bApO6oE-Mr1BhUtfZNq3OPBVfSL4ioQ9bHREfujIBXgk9LUDBAz2Sle7KjOr9HaUV16A4HBiaFIRFjsHPS9G8yEySp1m6F1CD_WR6apyUGgugRh_y39EcOJmxPOzmiac5DVM6fraA1VpcGbqrZ1x2ANHFDOfnYTycPtPNTgzE7LjkYjDDWbT03uN1YxfP4pqjDVRzY14pA8bSZ8ys-pDv5kUFCAsw-OlU4jYKUXp-M8_6KTaRQP71LPwppt__zG_NJPfy-qUil4pOU8_NoxtxerHgLLXbfExZdzfpoGinoEjn7nj7BJDEtl-LNeNEb5c-1ZymNfVMp-Cs3fLEPkAV8rtHFtZ0MhE_07GKbGo7hTrOmkM4DydxmHsdWGNbXAG35cprslEA5P7p3GHFKnRs5hGs2eq-XcZ3yki64ZBOU_Tv6UR7nUH09gF1rdrJo3dpre6M00COwwdZ02zUP5KxCuI8FKu2jsZu9zgMVXDALsdtM5orTCVLXsn4rddWd111zE-vMjNmMMmktW2cHMjH7j1ooA-9P083koNVYiLi4UhMA64gTqgyl8MxkZekl7eFSMa7qk295NaHOKtFxzYYcZ9jdioCwSPSZ0ZZWLoNgrK7SWfRh0uaTHNcMZ3wq8ae6CguktIeVTCPTQAqJLQqd7AU0oOCKCJ7BWnC-L8UC6m7Pm9ZS958uUVeWBhgKHzMAGq9UeQB7IEeAcbMn3EDgOSfd8qCb8iwU9iG9dcu9axQwWU7pd7kd-T-He61W7z5wWgpx1KehWCxrN6kuKSo6p-uUfwVnJukreOn8BJNAzADQgz68bhmN9VGih7YcKVnLgwDwKditrjSd6-tfE0Baarj3jWENvT6ohY17R9FDrKS-2v8IIX6tGjoKJw8SRhaWLNv4vWlmxRgR0gdac3qumd0GKqsWSveNz01naA==\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Now reply exactly with: Done.\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":40,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqB-kOX_0QAeoEksgNjwSbtGmVEQuMj5ODcFV6b7Kp3E8RoHRRmSXtRH0rtNbZRbhKz5jM48DUpDI1WTeO2HqCd_A3fsSgFxp5ACGVFjPWjfvP2JMdDkpoOo5gu2zy7WsWY0fseocQQ5q_jfG6SWw0fyaeeqfdQ9HkcHyg6gVEl5skb4L8_2lD5nClmLlNVVh5JCuXRH9eYysrfO19NOZ29A2MVUX-XgB6mmK5uSb1jE43GhrEPPYrMbB5JyzM6B-yeB8rE4H2wx530hQqtxwSZREa8G03rzTJ49_KAPWl0djGDDtufUX-t4EpBHo6loA3PMuiZ3VsJTkkPpEqkm6QQyAVkQ_8AdRu12CqHbFdu73I-BnArzr33yW6reNUjnZjFV5bWDyxIMh6ljy3O_2nGk-qdTLt6bGJbEjTdPj1hi7icYZTVPqofPU4pjlo9BnIBheo-4u9pA26V9G9vDAtM4myDdMEe4pnieUztBUYPUOVMaG2U9gqtNs6iPehKo3BeKy4lhYPorL2OPmf1lVUQOCW1MBbwT5xt1kjOVw7LggnyjrBsXVvDBWg0AFcvm14r3ZQezPgLetQfSx56mVEJpui9BuVSUg2Xvqb5tCCip6TipUVvzZJKKkxN43o8N6UVXLIn6wgstAn9727JgBEsjMxzvuOWaaI-qM3dWMcFzFSGvKb6gTiF37AhSOzosf31hnsGx8AnGmLmbuW3IMhZXZZMHgVUHx4p8pNRPeoCE-Bv833KZbRfVxe6tbmkLadBjXYaCQqPXDHBR7qbpfu4_M9UAy0kpic-joepxQKsCT2t6vsNaThRYaN3PtIgjs5xxAfa9yKeIYak8yiP96CUlME9Y7zaOIydPWYBhWLf3phQsWMax9eKdLDh19f0Y0iAJPpk--1xd2OVTuDxKIMpA==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXqW5MInCBPKulZ1lizyFtOaKUpKgHldAXVjTTs4XFE45gtxC1NbJoOi2tHoQhpfq-JGxtjSQEDTHnMCiLLhyqvQ4GlWVF4n51xcFVC_WgymkZqDxG6xPw8ITAsRI5vb8HiPO6EmmKt6xGIVXOjrrxRNAY3xtrByeYSvnCa6FDUHEkMeXmwllBalCeQPNDPl0Ub2ehuchNG0loMVLJoOjT-2KgDrXlOa6rCn3nUf4U1W5JA_kHytlgrD0IPbs7nY8wemdynJRXBoNSOT_U3nQSB6j-i4KIJAdLiUs9LVWMYleqmFQNs8S4dC3i5DfpHXWUMZ5Ai1d3gbvMP8bH7fsUyfIhyiDUvlgr6PZ9rfh8JqkjOpiQ7NFtSDuHQGdx__W3qi23WPDp3iQjKxVl1oUXfbMzsPE4bmNN9dnJ9qTQ43kvw8GyrGrSqRS8jCKuk9bxqeR_ibj4KoDdxvVbUeGMg3WKANfCRsNXlxwYtMpu3I4HxKm5EuMNKDg_e9RFH2wFEDm9wCKMZrC_5LShgKhSfhsk3yJ47Mit0zYdX27kyHGlZvpzLPYKdtHW1O15KNT4gFKBrIguCtjXz2Lb42ENM6Jo8BTY7BZbf0hXZ4A5mFNl_gyLVHWEHpR1GcYiJbs9RtQ-7qX_PTeZ11iFY3a7_jM7TK3WEN2IuG0OKbZVHvOkVcvyBEgIbzSzCzhtC-j584knI4WmYiqnltuwRcR2N3sxYY3vMcYGA2_AU5kYlZcJcztapTTW-aKbyGxPcw5D_dqb5mGpDqJgquye-qOufDt4Fd7cSc-g8awqR8QPsLz-ZDPLMB9JKQ3VqLQlNCKDUoDodGOAL3h-7EQG66osALfhpdsWcNmuVqlb0lNAXklrsZJtRKBU4pJ1UGCyVDwde7nv6I9PW19VumaRrJhc2cC52qUoyihvUo8xJsElaFp7-EHn5ymS4znZhRfyA_4UDL4rwj-3DqbMwNeDJrgBc3w==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Done\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"7tyE7hMvNOTM\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"obfuscation\":\"RGXvuTTSJS3AT5E\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Done.\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0a0794dab3b8ec7d016a1235e991c88195a4d2f9766babd985\",\"object\":\"response\",\"created_at\":1779578345,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578346,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":40,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_0a0794dab3b8ec7d016a1235ea4dd88195a32179255ed6c532\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXq8oliF2VeqiOUi-jUdi49emjffD6wtbmxlwQWbJ6tSxXIjyXvCeclOqKx83G83GyDOJqvR4L_D8V_ebJtgG87ahWB8Rr9LEQDoLT24n4Vz279xtHMxEGgv7f0NmaXu2dGFeFY_s2RhH-DqNE7V4nEkS7odJOTkhxTKgEcxtz3dDlEnGU7IgN2sD1lh9y90BD3ysvARegy4Cs0DhUjLOvkx11G9lk5dQ3yo1ek8JhTHpnVSYrLDYIudCh6pfu1yP1tx8xbxDHUcwlNclU9Hp_9ils5FhZNWC_tiLDscXXvRPBgMF77jdOicCV6cyUV0Snsu1_KSRbm4rLtgXLXVMqFyYpxdyicsD577e4yZ0VVXT4Oo_af0eDh3I3ZPIWui38EmYuoRhvQuYZkqjhGd_xOkvjQF4_Tp6cyNO0XdAMGMoYG-5npHC0gcPpv56qYGX8ffj0P8ZyR9shn3H7kcQqE2YXXBa42VKK0poPbC996xSqFNW7ygePel41h493XlJ70wnP50vFY5s0raNFf9eLP3YYmLxiPks9gshayGwUQXNNwrSimoQv3OeJzRzihbzZNWTfhR4xKs53nlXMjwnnXwHRH5D07vJg_1zU7BQzJ-QRLZnsnhIOq3psHt1yuoCtsSTKBN6HPiR81F-snIttJiUAiYsgv_ajwPxxnKP0FnFXQfBuaUAtAOD5G_3MC1yECjzq-YI4MDOXj4dsIGnHkdzXo-DV2lXMl2WnPqytoUkugp14SWbJso-eDsN5QivqspnYc1VsdNAaOOgjBiHmi-bACI1CykrkuiYJm1nOHAH4L4IQjpd0pcNm-Dk7z9LGIE5lwKI07hLXp_ByhVXRT8xWuugl43pzoM1jgYD4LjTjScC3ymauqqvKjjoHfnt0Zma0eVDeQrnVT6W9RQ9wDt5KVebrrwJTqlaNV0HywZJo3gwFy-Qq5MfwAwwC-GdjMsER1TgXO_E5kFZZD4sNVgw==\",\"summary\":[]},{\"id\":\"msg_0a0794dab3b8ec7d016a1235eaae648195ab7ad5385b641107\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Done.\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":35,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":20,\"output_tokens_details\":{\"reasoning_tokens\":12},\"total_tokens\":55},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + } + } + ] +} diff --git a/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json new file mode 100644 index 00000000000..107192d951e --- /dev/null +++ b/packages/llm/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "metadata": { + "name": "openai-responses/openai-responses-gpt-5-5-reasoning", + "recordedAt": "2026-05-23T23:19:03.175Z", + "provider": "openai", + "route": "openai-responses", + "transport": "http", + "model": "gpt-5.5", + "tags": ["prefix:openai-responses", "provider:openai", "flagship", "reasoning", "golden"] + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://api.openai.com/v1/responses", + "headers": { + "content-type": "application/json" + }, + "body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"system\",\"content\":\"Show concise reasoning when the provider supports visible reasoning summaries.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Think briefly, then reply exactly with: Hello!\"}]}],\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"reasoning\":{\"effort\":\"low\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"max_output_tokens\":120,\"stream\":true}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "text/event-stream; charset=utf-8" + }, + "body": "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_05f7e55dc1d2d743016a1235e5bb3c8193b4c0e30fa316700c\",\"object\":\"response\",\"created_at\":1779578341,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_05f7e55dc1d2d743016a1235e5bb3c8193b4c0e30fa316700c\",\"object\":\"response\",\"created_at\":1779578341,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"rs_05f7e55dc1d2d743016a1235e65eac8193ba914b956e3a49e1\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXmpkIEr1Ox1CgTHFUzuGnLwOE9YZFf7lBDRcOgQiNuTk-CopgZ20-kaosULz5cy40Ga0zkuwMFxLHSH4Cyebm9mQCMMZmzZQybp-s-yfNGm9zz7kAh_y6NbaAQZiI2iaTY5_sQwIrMsdQ56-T3_--unhlXVjEqMwdbsQOLpTg0KpAigUOJkuAl4PgCHsr6p_oTIZ5ycChpihxh6Oyuyf_YzLz-bL63kWZeu2bZFYE3WftnJNzcY5VjaNEVDlYY5IBbgCqOCc8OhLrYarwaPdhiizu1FIuhreEDiYurEQOa5jSW3wN2KGUhfDPurzguIbZ-ZhSRW58w9UgrhYOS30bQkm3EYd7w9nI5MteZruw3hIVKR__tU_2T2o7GsTxYNYLgn7Q8WoBASZrBtDpXuBM5jO2JQzDEFPG9_S8f44_LDgKInHgh-t32eV1ZNW08Pg9l0tUutiqdEVbDFKFB68Eu5PCKRKqZ18VzXI2X_YeU0DW9ld_GoSesI88rmWCmOta2AQOrVNa2uehlMRXb0wGXKzS6t31di0_PQwPj9rBdVk-N3S-FOlF5fFEGDqWBR3E0lrEOtNMeIuR8u3gGDbEacqL-Sz22Mi7AJou7trOnVH5qFqbKpfOJnxpzjOeWUYVDpmVFD81Om7Fc6zlZsv0KIEt-w5IE_dXi2_guUpu0JhowDWFI9aFcRVZ6A9z7Mrwqtkrs9QqRJws2BRUde-6MOryUcKWGfwdkNMRkAVHSlC6T3Yq44UtLqb1gJ-vasc2K9BZwRtYBVy0jif1tjrMviiNy9ltdxPVlIJ-R_4JcYtcbRQc1ovd-aQ9MprDatYE3Vnmk_O_WSnzHRxzPbftBHb_IWwir4G3lTOTi7xopkmVFidgl03h72BfbmwkzKXlMmvClRYP5u-mYyZoG50h9ag==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":2}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"rs_05f7e55dc1d2d743016a1235e65eac8193ba914b956e3a49e1\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXn9Xij0jt39Rh_0pwIWNN4jY3e6aD8Z0BBuhH_kgkhr6VNIvsDQdD8q9ju8vkF4JnhDqcfSIbdtX_8r4HHiV7O6kFEHRSiW23Z775HdB6fggaELggnLAcBN4MxzN618I2CV-pnNStMVMeUZtbV22l6o0F6n00-_5vm7KkP44JQRFrYj_iypI9AY0S0OqItcAiB6ERLLdKTJELOAzGxyeHKpQeWH3kQY9ZbSFLfUVooZjZEEBBB3sLMkvkDq6J9iIyQFKXZeo_oFvayxWFD8BwW3fVuUFcg0waG5TJwHA90iPFjOCddkk4Kn07cIwoyuM8xYquLPQ2x3jBLninoP8UyRXrQaDFR3maPtQ4HbNBf9cz5nZmXmjmZnO9Ee78ELcU_LkvmTgtozd094XVxLHg9DN-Y_s-1GMD9SgiqBNePe-1f_Sf2FD-CQsE-vDIcpmlLwxrhkXkwZ8Ws4k8NhA1Kq8DA5KYmz7S2akl2Vg2wr5ZrdH1XQBarWUIwlhTFALhTipDPinqhytnxm5MEmu09MBGXYOP5qNQ2hT2F_ibH7kM4kBQV5zVLDl6U2ZsKDt_r7F9in8YU5ffLYAd_4M9FgsJQUvC1CxLRSSQyyPKbdy0mYCrBLTo68FGV36JIj_SQBYHp9I5IIXfMfw1d78SNN_jtgs8JweLVNhOzoOFqMEYgO4zO6xa6r7E1F6YtuQ3oQ8OIEjXIeWeLsRGn51hNYA9s2hfm8Qh14rq5TMV9QhDXj0a-7FO6PIqTUr2ZqfB6Oqeb_Ut708MWtIjZ0oRSYkDddS8QzcwGbg1AFwF9okVtZZqXyItZseC9hKQ7vjh9pWaMrsKT8pYxjbB1fwKPgwTZHGzQzhkLXsp7btR5DpRP4h1-YHTsiMDlEdjtZVF0Zn7OuKTQlanB14FsmaaJq-Ty0pktr9ejKji3VmmDeIWTWKmIMvnp9nc3_Wl_Cbvschquknk7Hd12KZkyRaWq-Q==\",\"summary\":[]},\"output_index\":0,\"sequence_number\":3}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":4}\n\nevent: response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":5}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"Hello\",\"item_id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"logprobs\":[],\"obfuscation\":\"rArO0JBBk8l\",\"output_index\":1,\"sequence_number\":6}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"logprobs\":[],\"obfuscation\":\"WgBYGB6udiKMN2k\",\"output_index\":1,\"sequence_number\":7}\n\nevent: response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":8,\"text\":\"Hello!\"}\n\nevent: response.content_part.done\ndata: {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"},\"sequence_number\":9}\n\nevent: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":10}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_05f7e55dc1d2d743016a1235e5bb3c8193b4c0e30fa316700c\",\"object\":\"response\",\"created_at\":1779578341,\"status\":\"completed\",\"background\":false,\"completed_at\":1779578343,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":120,\"max_tool_calls\":null,\"model\":\"gpt-5.5-2026-04-23\",\"moderation\":null,\"output\":[{\"id\":\"rs_05f7e55dc1d2d743016a1235e65eac8193ba914b956e3a49e1\",\"type\":\"reasoning\",\"encrypted_content\":\"gAAAAABqEjXnFOrSKMBad2Atutni2Bxu4oRnudNwadGzwBCliXn-ocbCrlL6URwbrS22k21EaNPztTRWQVOGMzldY1PrMyJQZxj479kaAPXFbXir2xv5QfL6ykVcNNb5yn8eV5flXuOPfRM1dkosOD5QtlEhOvFbW14i-WhbjU1fpTiDCbLIotPo8tkuIk-zJMSgA0jlE73Rmz2BQlT-ZSTqJA7f9hT93QMYjsWNCslk40WCbmqo4dJuuf19qg_2ktrXu5yjmde8QU-blxhPKiDsMlj4rFA1F6_pNqJ_oUgU6mwIuUDIVPLxIaV3IBDS9j9XjiBSzwtFAsotlqWv_D1vWGUKSGPQKPs31ylYvBVQMTOdXxQZ6TKPveYybka8XbAEVNTFAJjg5PDRKWikfqqH4b0W_FFVNCSSf1UIgLh2-Hf-xMRBXozAFkCoj8M_wZOqj-gYoIsaoLnm7Z15rMrNiL2aMgNc1-mB1Iw1rFTo7hdiLiEj7C0rmm77CrxkFfQgpWsVAw94xpbVO7oC4Xiy1UNB-VCCb5FLNAHyJyLd_wc0SVuEsxjllLZ-q3unHYDnZO-zuxhjZ2jodX_7tbM6PCKul5ETSxtWQztzZD3MZ8ED_kVagXNNth-BTuTB4W9678cQbkSZK0DzV50H2_NJuRgQdCROnVKiiA2mzAOl7LXzDLTdTlHOQYRKt3wvaFI1DkVD0T0_Wj3z9lI2kgx6ExtOvU9nkqnPv4gf1x2ce8Ao9uf8NgtQbqhpeGMoU0Km9AALkoCtiyIXYJWNVVYAi9JA9QS05K-S__J_GlgFg7J3_YiH8bcmMrfwryn9K6yelsLt3SBKCA9pkLVI-HFDRx19brkAQRZRlaflF7qQApps8MQBGGrvrKTui_4EAABhFmjsn8XVIGoz7f_MFoS6ijdi8LCHEf8MNquinKURmb8Cdg1Zekrbl6KGsdcPztftMKH-sO6bEWZeUWJb2KqtIDnH5jr-Qw==\",\"summary\":[]},{\"id\":\"msg_05f7e55dc1d2d743016a1235e7171481938e1402264186381c\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"Hello!\"}],\"phase\":\"final_answer\",\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"24h\",\"reasoning\":{\"context\":\"current_turn\",\"effort\":\"low\",\"summary\":\"detailed\"},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"low\"},\"tool_choice\":\"auto\",\"tools\":[],\"top_logprobs\":0,\"top_p\":0.98,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":31,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":21,\"output_tokens_details\":{\"reasoning_tokens\":13},\"total_tokens\":52},\"user\":null,\"metadata\":{}},\"sequence_number\":11}\n\n" + } + } + ] +} diff --git a/packages/llm/test/generate-object.test.ts b/packages/llm/test/generate-object.test.ts index 66e39f77703..9606f58f984 100644 --- a/packages/llm/test/generate-object.test.ts +++ b/packages/llm/test/generate-object.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import { Effect, Schema } from "effect" import { LLM } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" +import { Auth } from "../src/route" import { Tool, toDefinitions } from "../src/tool" import { it } from "./lib/effect" import { dynamicResponse } from "./lib/http" @@ -17,11 +18,9 @@ type OpenAIChatBody = { }> } -const model = OpenAIChat.model({ - id: "gpt-4o-mini", - baseURL: "https://api.openai.test/v1/", - headers: { authorization: "Bearer test" }, -}) +const model = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-4o-mini" }) const Json = Schema.fromJsonString(Schema.Unknown) const decodeJson = Schema.decodeUnknownSync(Json) diff --git a/packages/llm/test/lib/http.ts b/packages/llm/test/lib/http.ts index cfe7e6883be..f6c600555b9 100644 --- a/packages/llm/test/lib/http.ts +++ b/packages/llm/test/lib/http.ts @@ -1,8 +1,9 @@ import { Effect, Layer, Ref } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { LLMClient, RequestExecutor } from "../../src/route" +import { LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import type { Service as LLMClientService } from "../../src/route/client" import type { Service as RequestExecutorService } from "../../src/route/executor" +import type { Service as WebSocketExecutorService } from "../../src/route/transport/websocket" export type HandlerInput = { readonly request: HttpClientRequest.HttpClientRequest @@ -31,12 +32,13 @@ const handlerLayer = (handler: Handler): Layer.Layer => ), ) -export type RuntimeEnv = RequestExecutorService | LLMClientService +export type RuntimeEnv = RequestExecutorService | WebSocketExecutorService | LLMClientService export const runtimeLayer = (layer: Layer.Layer): Layer.Layer => { const requestExecutorLayer = RequestExecutor.layer.pipe(Layer.provide(layer)) - const llmClientLayer = LLMClient.layer.pipe(Layer.provide(requestExecutorLayer)) - return Layer.mergeAll(requestExecutorLayer, llmClientLayer) + const deps = Layer.mergeAll(requestExecutorLayer, WebSocketExecutor.layer) + const llmClientLayer = LLMClient.layer.pipe(Layer.provide(deps)) + return Layer.mergeAll(deps, llmClientLayer) } const SSE_HEADERS = { "content-type": "text/event-stream" } as const diff --git a/packages/llm/test/lib/tool-runtime.ts b/packages/llm/test/lib/tool-runtime.ts index a12941603a1..5c98b78435b 100644 --- a/packages/llm/test/lib/tool-runtime.ts +++ b/packages/llm/test/lib/tool-runtime.ts @@ -1,4 +1,3 @@ -import { Stream } from "effect" import { LLMClient } from "../../src/route" import type { Tools } from "../../src/tool" import type { RunOptions } from "../../src/tool-runtime" diff --git a/packages/llm/test/llm.test.ts b/packages/llm/test/llm.test.ts index a20c48411eb..007b602ce35 100644 --- a/packages/llm/test/llm.test.ts +++ b/packages/llm/test/llm.test.ts @@ -1,18 +1,23 @@ import { describe, expect, test } from "bun:test" import { LLM, LLMResponse } from "../src" -import { LLMRequest, Message, ModelRef, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema" +import * as OpenAIChat from "../src/protocols/openai-chat" +import * as OpenAIResponses from "../src/protocols/openai-responses" +import { LLMRequest, Message, Model, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema" + +const chatRoute = OpenAIChat.route +const responsesRoute = OpenAIResponses.route describe("llm constructors", () => { test("builds canonical schema classes from ergonomic input", () => { const request = LLM.request({ id: "req_1", - model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }), + model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), system: "You are concise.", prompt: "Say hello.", }) expect(request).toBeInstanceOf(LLMRequest) - expect(request.model).toBeInstanceOf(ModelRef) + expect(request.model).toBeInstanceOf(Model) expect(request.messages[0]).toBeInstanceOf(Message) expect(request.system).toEqual([{ type: "text", text: "You are concise." }]) expect(request.messages[0]?.content).toEqual([{ type: "text", text: "Say hello." }]) @@ -23,7 +28,7 @@ describe("llm constructors", () => { test("updates requests without spreading schema class instances", () => { const base = LLM.request({ id: "req_1", - model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }), + model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), prompt: "Say hello.", }) const updated = LLM.updateRequest(base, { @@ -38,16 +43,16 @@ describe("llm constructors", () => { expect(updated.messages.map((message) => message.role)).toEqual(["user", "assistant"]) }) - test("keeps request options separate from model defaults", () => { + test("keeps request options separate from route defaults", () => { const request = LLM.request({ - model: LLM.model({ + model: Model.make({ id: "fake-model", provider: "fake", - route: "openai-chat", - baseURL: "https://fake.local", - generation: { maxTokens: 100, temperature: 1 }, - providerOptions: { openai: { store: false, metadata: { model: true } } }, - http: { body: { metadata: { model: true } }, headers: { "x-shared": "model" }, query: { model: "1" } }, + route: chatRoute.with({ + generation: { maxTokens: 100, temperature: 1 }, + providerOptions: { openai: { store: false, metadata: { model: true } } }, + http: { body: { metadata: { model: true } }, headers: { "x-shared": "model" }, query: { model: "1" } }, + }), }), prompt: "Say hello.", generation: { temperature: 0 }, @@ -67,7 +72,7 @@ describe("llm constructors", () => { test("updates canonical requests from the request datatype", () => { const base = LLM.request({ id: "req_1", - model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }), + model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }), prompt: "Say hello.", }) const updated = LLMRequest.update(base, { messages: [...base.messages, Message.assistant("Hi.")] }) @@ -80,14 +85,18 @@ describe("llm constructors", () => { }) test("updates canonical models from the model datatype", () => { - const base = LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }) - const updated = ModelRef.update(base, { route: "openai-responses" }) + const base = Model.make({ + id: "fake-model", + provider: "fake", + route: chatRoute, + }) + const updated = Model.update(base, { route: responsesRoute }) - expect(updated).toBeInstanceOf(ModelRef) + expect(updated).toBeInstanceOf(Model) expect(String(updated.id)).toBe("fake-model") - expect(updated.route).toBe("openai-responses") - expect(String(ModelRef.input(updated).provider)).toBe("fake") - expect(ModelRef.update(updated, {})).toBe(updated) + expect(updated.route).toBe(responsesRoute) + expect(String(Model.input(updated).provider)).toBe("fake") + expect(Model.update(updated, {})).toBe(updated) }) test("builds tool choices from names and tools", () => { @@ -105,7 +114,11 @@ describe("llm constructors", () => { expect(ToolChoice.make("required")).toEqual(new ToolChoice({ type: "required" })) expect( LLM.request({ - model: LLM.model({ id: "fake-model", provider: "fake", route: "openai-chat", baseURL: "https://fake.local" }), + model: Model.make({ + id: "fake-model", + provider: "fake", + route: chatRoute, + }), prompt: "Use tools if needed.", toolChoice: "required", }).toolChoice, diff --git a/packages/llm/test/provider.types.ts b/packages/llm/test/provider.types.ts index a04ce8bc609..f8b46e37539 100644 --- a/packages/llm/test/provider.types.ts +++ b/packages/llm/test/provider.types.ts @@ -1,9 +1,9 @@ import { Provider } from "../src/provider" -import { ProviderID, type ModelRef } from "../src/schema" +import { ProviderID, type Model } from "../src/schema" -declare const model: (id: string) => ModelRef -declare const requiredModel: (id: string, options: { readonly baseURL: string }) => ModelRef -declare const chat: (id: string, options: { readonly apiKey: string }) => ModelRef +declare const model: (id: string) => Model +declare const requiredModel: (id: string, options: { readonly baseURL: string }) => Model +declare const chat: (id: string, options: { readonly apiKey: string }) => Model Provider.make({ id: ProviderID.make("example"), @@ -22,6 +22,8 @@ const requiredProvider = Provider.make({ model: requiredModel, }) +// Provider.make is advanced structural typing coverage; built-in providers use +// configure(...).model(id) facades instead of second-argument selectors. requiredProvider.model("custom", { baseURL: "https://example.com/v1" }) // @ts-expect-error Provider.make preserves required model options. diff --git a/packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts b/packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts index 68b7e0a4ae1..1044612b7a7 100644 --- a/packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts +++ b/packages/llm/test/provider/anthropic-messages-cache.recorded.test.ts @@ -3,14 +3,13 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { CacheHint, LLM } from "../../src" import { LLMClient } from "../../src/route" -import * as AnthropicMessages from "../../src/protocols/anthropic-messages" +import * as Anthropic from "../../src/providers/anthropic" import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios" import { recordedTests } from "../recorded-test" -const model = AnthropicMessages.model({ - id: "claude-haiku-4-5-20251001", +const model = Anthropic.configure({ apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture", -}) +}).model("claude-haiku-4-5-20251001") // Two identical generations in a row. The first call writes the prefix into // Anthropic's cache; the second should report a cache read against the same diff --git a/packages/llm/test/provider/anthropic-messages.recorded.test.ts b/packages/llm/test/provider/anthropic-messages.recorded.test.ts index 5fefae51d40..7afcdcfda00 100644 --- a/packages/llm/test/provider/anthropic-messages.recorded.test.ts +++ b/packages/llm/test/provider/anthropic-messages.recorded.test.ts @@ -3,14 +3,13 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { LLM, LLMError, Message, ToolCallPart } from "../../src" import { LLMClient } from "../../src/route" -import * as AnthropicMessages from "../../src/protocols/anthropic-messages" +import * as Anthropic from "../../src/providers/anthropic" import { weatherToolName } from "../recorded-scenarios" import { recordedTests } from "../recorded-test" -const model = AnthropicMessages.model({ - id: "claude-haiku-4-5-20251001", +const model = Anthropic.configure({ apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture", -}) +}).model("claude-haiku-4-5-20251001") const malformedToolOrderRequest = LLM.request({ id: "recorded_anthropic_malformed_tool_order", diff --git a/packages/llm/test/provider/anthropic-messages.test.ts b/packages/llm/test/provider/anthropic-messages.test.ts index 71204bcd639..5198af9ab76 100644 --- a/packages/llm/test/provider/anthropic-messages.test.ts +++ b/packages/llm/test/provider/anthropic-messages.test.ts @@ -1,17 +1,17 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" +import { HttpClientRequest } from "effect/unstable/http" import { CacheHint, LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" -import { LLMClient } from "../../src/route" +import { Auth, LLMClient } from "../../src/route" import * as AnthropicMessages from "../../src/protocols/anthropic-messages" +import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios" import { it } from "../lib/effect" -import { fixedResponse } from "../lib/http" +import { dynamicResponse, fixedResponse } from "../lib/http" import { sseEvents } from "../lib/sse" -const model = AnthropicMessages.model({ - id: "claude-sonnet-4-5", - baseURL: "https://api.anthropic.test/v1/", - headers: { "x-api-key": "test" }, -}) +const model = AnthropicMessages.route + .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) + .model({ id: "claude-sonnet-4-5" }) const request = LLM.request({ id: "req_1", @@ -24,6 +24,19 @@ const request = LLM.request({ generation: { maxTokens: 20, temperature: 0 }, }) +type AnthropicToolResult = Extract< + AnthropicMessages.AnthropicMessagesBody["messages"][number]["content"][number], + { readonly type: "tool_result" } +> + +const expectToolResult = (body: AnthropicMessages.AnthropicMessagesBody): AnthropicToolResult => { + const result = body.messages + .flatMap((message) => (message.role === "user" ? message.content : [])) + .find((block): block is AnthropicToolResult => block.type === "tool_result") + expect(result).toBeDefined() + return result! +} + describe("Anthropic Messages route", () => { it.effect("prepares Anthropic Messages target", () => Effect.gen(function* () { @@ -42,7 +55,7 @@ describe("Anthropic Messages route", () => { it.effect("prepares tool call and tool result messages", () => Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( + const prepared = yield* LLMClient.prepare( LLM.request({ id: "req_tool_result", model, @@ -71,6 +84,131 @@ describe("Anthropic Messages route", () => { }), ) + // Regression: screenshot/read tool results must stay structured so base64 + // image data is not JSON-stringified into `tool_result.content`. + it.effect("lowers image tool-result content as structured image blocks", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_image", + model, + messages: [ + Message.user("Show me the screenshot."), + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { filePath: "shot.png" } })]), + Message.tool({ + id: "call_1", + name: "read", + resultType: "content", + result: [ + { type: "text", text: "Image read successfully" }, + { type: "media", mediaType: "image/png", data: "AAECAw==" }, + ], + }), + ], + cache: "none", + }), + ) + + expect(expectToolResult(prepared.body).content).toEqual([ + { type: "text", text: "Image read successfully" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, + ]) + }), + ) + + it.effect("lowers single-image tool-result content as a structured image block", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_image_only", + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "screenshot", input: {} })]), + Message.tool({ + id: "call_1", + name: "screenshot", + resultType: "content", + result: [{ type: "media", mediaType: "image/jpeg", data: "/9j/AA==" }], + }), + ], + cache: "none", + }), + ) + + expect(expectToolResult(prepared.body).content).toEqual([ + { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "/9j/AA==" } }, + ]) + }), + ) + + it.effect("rejects non-image media in tool-result content with a clear error", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_unsupported_media", + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "fetch", input: {} })]), + Message.tool({ + id: "call_1", + name: "fetch", + resultType: "content", + result: [{ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" }], + }), + ], + cache: "none", + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("Anthropic Messages") + expect(error.message).toContain("audio/mpeg") + }), + ) + + it.effect("prepares the composed native continuation request", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + continuationRequest({ + id: "req_native_continuation_anthropic", + model, + features: nativeAnthropicMessagesContinuation, + }), + ) + + expect(prepared.body).toMatchObject({ + system: [{ type: "text", text: "You are concise. Continue from the provided history." }], + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is shown here?" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, + ], + }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "I inspected the previous turn.", signature: "sig_continuation_1" }, + { type: "text", text: "It shows a small test image." }, + ], + }, + { role: "user", content: [{ type: "text", text: "Check the weather in Paris before continuing." }] }, + { + role: "assistant", + content: [{ type: "tool_use", id: "call_weather_1", name: "get_weather", input: { city: "Paris" } }], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call_weather_1", content: '{"temperature":22}' }], + }, + { role: "assistant", content: [{ type: "text", text: "Paris is 22 degrees." }] }, + { role: "user", content: [{ type: "text", text: "Continue from this conversation in one short sentence." }] }, + ], + }) + expect(prepared.body.tools).toEqual([expect.objectContaining({ name: "get_weather" })]) + }), + ) + it.effect("lowers preserved Anthropic reasoning signature metadata", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( @@ -199,7 +337,29 @@ describe("Anthropic Messages route", () => { ), ) - expect(response.events).toEqual([{ type: "provider-error", message: "Overloaded" }]) + // Prefix the error type so consumers can distinguish overloads, rate + // limits, and quota errors without parsing the message string. + expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error: Overloaded" }]) + }), + ) + + it.effect("falls back to error type when no message is present", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "" } }))), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "overloaded_error" }]) + }), + ) + + it.effect("falls back to a stable default when error payload is absent", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "error" }))), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "Anthropic Messages stream error" }]) }), ) @@ -394,17 +554,51 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("rejects unsupported user media content", () => + it.effect("continues a conversation with user image content", () => Effect.gen(function* () { - const error = yield* LLMClient.prepare( + const response = yield* LLMClient.generate( LLM.request({ id: "req_media", model, - messages: [Message.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })], + messages: [ + Message.user([ + { type: "text", text: "What is in this image?" }, + { type: "media", mediaType: "image/png", data: "AAECAw==" }, + ]), + ], }), - ).pipe(Effect.flip) + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(yield* Effect.promise(() => web.json())).toMatchObject({ + messages: [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } }, + ], + }, + ], + }) + return input.respond( + sseEvents( + { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }, + { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "An image." } }, + { type: "content_block_stop", index: 0 }, + { type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 3 } }, + { type: "message_stop" }, + ), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) - expect(error.message).toContain("Anthropic Messages user messages only support text content for now") + expect(response.text).toBe("An image.") }), ) diff --git a/packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts b/packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts index 2771046f80f..8702e4eb403 100644 --- a/packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts +++ b/packages/llm/test/provider/bedrock-converse-cache.recorded.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { CacheHint, LLM } from "../../src" import { LLMClient } from "../../src/route" -import * as BedrockConverse from "../../src/protocols/bedrock-converse" +import { AmazonBedrock } from "../../src/providers" import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios" import { recordedTests } from "../recorded-test" @@ -12,15 +12,14 @@ const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1" // doesn't reliably surface `cacheRead`/`cacheWrite` in usage, so the second // call wouldn't deterministically prove cache mapping works. Override with // BEDROCK_CACHE_MODEL_ID if your account has access elsewhere. -const model = BedrockConverse.model({ - id: process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0", +const model = AmazonBedrock.configure({ credentials: { region: RECORDING_REGION, accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture", secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture", sessionToken: process.env.AWS_SESSION_TOKEN, }, -}) +}).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0") const cacheRequest = LLM.request({ id: "recorded_bedrock_cache", diff --git a/packages/llm/test/provider/bedrock-converse.test.ts b/packages/llm/test/provider/bedrock-converse.test.ts index ffdd6e80082..a3d8c5c626f 100644 --- a/packages/llm/test/provider/bedrock-converse.test.ts +++ b/packages/llm/test/provider/bedrock-converse.test.ts @@ -4,7 +4,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { CacheHint, LLM, Message, ToolCallPart, ToolChoice } from "../../src" import { LLMClient } from "../../src/route" -import * as BedrockConverse from "../../src/protocols/bedrock-converse" +import { AmazonBedrock } from "../../src/providers" import { it } from "../lib/effect" import { fixedResponse } from "../lib/http" import { @@ -52,11 +52,10 @@ const eventStreamBody = (...payloads: ReadonlyArray) const fixedBytes = (bytes: Uint8Array) => fixedResponse(bytes.slice().buffer, { headers: { "content-type": "application/vnd.amazon.eventstream" } }) -const model = BedrockConverse.model({ - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", +const model = AmazonBedrock.configure({ baseURL: "https://bedrock-runtime.test", apiKey: "test-bearer", -}) +}).model("anthropic.claude-3-5-sonnet-20240620-v1:0") const baseRequest = LLM.request({ id: "req_1", @@ -156,6 +155,55 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("lowers image content in tool-result messages", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_image", + model, + messages: [ + Message.user("Capture the screen."), + Message.assistant([ToolCallPart.make({ id: "tool_1", name: "screenshot", input: {} })]), + Message.tool({ + id: "tool_1", + name: "screenshot", + result: { + type: "content", + value: [ + { type: "text", text: "Screenshot captured." }, + { type: "media", mediaType: "image/png", data: "AAAA" }, + ], + }, + }), + ], + cache: "none", + }), + ) + + expect(prepared.body).toMatchObject({ + messages: [ + { role: "user", content: [{ text: "Capture the screen." }] }, + { + role: "assistant", + content: [{ toolUse: { toolUseId: "tool_1", name: "screenshot", input: {} } }], + }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tool_1", + content: [{ text: "Screenshot captured." }, { image: { format: "png", source: { bytes: "AAAA" } } }], + status: "success", + }, + }, + ], + }, + ], + }) + }), + ) + it.effect("decodes text-delta + messageStop + metadata usage from binary event stream", () => Effect.gen(function* () { const body = eventStreamBody( @@ -249,39 +297,32 @@ describe("Bedrock Converse route", () => { it.effect("rejects requests with no auth path", () => Effect.gen(function* () { - const unsignedModel = BedrockConverse.model({ - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", + const unsignedModel = AmazonBedrock.configure({ baseURL: "https://bedrock-runtime.test", - }) + }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") const error = yield* LLMClient.generate(LLM.updateRequest(baseRequest, { model: unsignedModel })).pipe( Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "end_turn" }]))), Effect.flip, ) - expect(error.message).toContain("Bedrock Converse requires either model.apiKey") + expect(error.message).toContain("Bedrock Converse requires either route bearer auth or AWS credentials") }), ) it.effect("signs requests with SigV4 when AWS credentials are provided (deterministic plumbing check)", () => Effect.gen(function* () { - const signed = BedrockConverse.model({ - id: "anthropic.claude-3-5-sonnet-20240620-v1:0", + const signed = AmazonBedrock.configure({ baseURL: "https://bedrock-runtime.test", credentials: { region: "us-east-1", accessKeyId: "AKIAIOSFODNN7EXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", }, - }) + }).model("anthropic.claude-3-5-sonnet-20240620-v1:0") const prepared = yield* LLMClient.prepare(LLM.updateRequest(baseRequest, { model: signed })) expect(prepared.route).toBe("bedrock-converse") - // The prepare phase doesn't sign — toHttp does. We assert the credential - // is plumbed onto the model native field for the signer to find. - expect(prepared.model.native).toMatchObject({ - aws_credentials: { region: "us-east-1", accessKeyId: "AKIAIOSFODNN7EXAMPLE" }, - aws_region: "us-east-1", - }) + expect(prepared.model).toBe(signed) }), ) @@ -531,18 +572,17 @@ describe("Bedrock Converse route", () => { const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1" const recordedModel = () => - BedrockConverse.model({ + AmazonBedrock.configure({ // Most newer Anthropic models on Bedrock require a cross-region inference // profile (`us.` prefix). Nova does not require an Anthropic use-case form // and is on-demand-throughput accessible by default for most accounts. - id: process.env.BEDROCK_MODEL_ID ?? "us.amazon.nova-micro-v1:0", credentials: { region: RECORDING_REGION, accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture", secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture", sessionToken: process.env.AWS_SESSION_TOKEN, }, - }) + }).model(process.env.BEDROCK_MODEL_ID ?? "us.amazon.nova-micro-v1:0") const recorded = recordedTests({ prefix: "bedrock-converse", @@ -598,7 +638,6 @@ describe("Bedrock Converse recorded", () => { recorded.effect.with("drives a tool loop", { tags: ["tool", "tool-loop", "golden"] }, () => Effect.gen(function* () { - const llm = yield* LLMClient.Service expectWeatherToolLoop( yield* runWeatherToolLoop( weatherToolLoopRequest({ diff --git a/packages/llm/test/provider/cloudflare.test.ts b/packages/llm/test/provider/cloudflare.test.ts index 125e79bf9ef..acd63962946 100644 --- a/packages/llm/test/provider/cloudflare.test.ts +++ b/packages/llm/test/provider/cloudflare.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Schema } from "effect" import { HttpClientRequest } from "effect/unstable/http" import { LLM } from "../../src" -import * as Cloudflare from "../../src/providers/cloudflare" +import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare" import { LLMClient } from "../../src/route" import { it } from "../lib/effect" import { dynamicResponse } from "../lib/http" @@ -21,18 +21,18 @@ const deltaChunk = (delta: object, finishReason: string | null = null) => ({ describe("Cloudflare", () => { it.effect("prepares AI Gateway models through the OpenAI-compatible Chat protocol", () => Effect.gen(function* () { - const model = Cloudflare.aiGateway("workers-ai/@cf/meta/llama-3.3-70b-instruct", { + const model = CloudflareAIGateway.configure({ accountId: "test-account", gatewayId: "test-gateway", apiKey: "test-token", - }) + }).model("workers-ai/@cf/meta/llama-3.3-70b-instruct") expect(model).toMatchObject({ id: "workers-ai/@cf/meta/llama-3.3-70b-instruct", provider: "cloudflare-ai-gateway", - route: "cloudflare-ai-gateway", - baseURL: "https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat", + route: { id: "cloudflare-ai-gateway" }, }) + expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat") const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." })) @@ -49,11 +49,11 @@ describe("Cloudflare", () => { Effect.gen(function* () { const response = yield* LLM.generate( LLM.request({ - model: Cloudflare.aiGateway("openai/gpt-4o-mini", { + model: CloudflareAIGateway.configure({ accountId: "test-account", gatewayId: "test-gateway", apiKey: "test-token", - }), + }).model("openai/gpt-4o-mini"), prompt: "Say hello.", }), ).pipe( @@ -86,11 +86,11 @@ describe("Cloudflare", () => { it.effect("defaults AI Gateway id to default when omitted or blank", () => Effect.gen(function* () { expect( - Cloudflare.aiGateway("workers-ai/@cf/meta/llama-3.3-70b-instruct", { + CloudflareAIGateway.configure({ accountId: "test-account", gatewayId: "", gatewayApiKey: "test-token", - }).baseURL, + }).model("workers-ai/@cf/meta/llama-3.3-70b-instruct").route.endpoint.baseURL, ).toBe("https://gateway.ai.cloudflare.com/v1/test-account/default/compat") }), ) @@ -99,11 +99,11 @@ describe("Cloudflare", () => { Effect.gen(function* () { yield* LLM.generate( LLM.request({ - model: Cloudflare.aiGateway("openai/gpt-4o-mini", { + model: CloudflareAIGateway.configure({ accountId: "test-account", gatewayApiKey: "gateway-token", apiKey: "provider-token", - }), + }).model("openai/gpt-4o-mini"), prompt: "Say hello.", }), ).pipe( @@ -129,31 +129,31 @@ describe("Cloudflare", () => { Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: Cloudflare.aiGateway("openai/gpt-4o-mini", { + model: CloudflareAIGateway.configure({ baseURL: "https://gateway.proxy.test/v1/custom/compat", apiKey: "test-token", - }), + }).model("openai/gpt-4o-mini"), prompt: "Say hello.", }), ) - expect(prepared.model.baseURL).toBe("https://gateway.proxy.test/v1/custom/compat") + expect(prepared.model.route.endpoint.baseURL).toBe("https://gateway.proxy.test/v1/custom/compat") }), ) it.effect("prepares direct Workers AI models through the OpenAI-compatible Chat protocol", () => Effect.gen(function* () { - const model = Cloudflare.workersAI("@cf/meta/llama-3.1-8b-instruct", { + const model = CloudflareWorkersAI.configure({ accountId: "test-account", apiKey: "test-token", - }) + }).model("@cf/meta/llama-3.1-8b-instruct") expect(model).toMatchObject({ id: "@cf/meta/llama-3.1-8b-instruct", provider: "cloudflare-workers-ai", - route: "cloudflare-workers-ai", - baseURL: "https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1", + route: { id: "cloudflare-workers-ai" }, }) + expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1") const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." })) @@ -170,10 +170,10 @@ describe("Cloudflare", () => { Effect.gen(function* () { const response = yield* LLM.generate( LLM.request({ - model: Cloudflare.workersAI("@cf/meta/llama-3.1-8b-instruct", { + model: CloudflareWorkersAI.configure({ accountId: "test-account", apiKey: "test-token", - }), + }).model("@cf/meta/llama-3.1-8b-instruct"), prompt: "Say hello.", }), ).pipe( @@ -205,9 +205,9 @@ describe("Cloudflare", () => { Effect.gen(function* () { yield* LLM.generate( LLM.request({ - model: Cloudflare.workersAI("@cf/meta/llama-3.1-8b-instruct", { + model: CloudflareWorkersAI.configure({ accountId: "test-account", - }), + }).model("@cf/meta/llama-3.1-8b-instruct"), prompt: "Say hello.", }), ).pipe( diff --git a/packages/llm/test/provider/gemini-cache.recorded.test.ts b/packages/llm/test/provider/gemini-cache.recorded.test.ts index b86980c43da..d210c5c024f 100644 --- a/packages/llm/test/provider/gemini-cache.recorded.test.ts +++ b/packages/llm/test/provider/gemini-cache.recorded.test.ts @@ -2,14 +2,13 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { LLM } from "../../src" import { LLMClient } from "../../src/route" -import * as Gemini from "../../src/protocols/gemini" +import * as Google from "../../src/providers/google" import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios" import { recordedTests } from "../recorded-test" -const model = Gemini.model({ - id: "gemini-2.5-flash", +const model = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? process.env.GEMINI_API_KEY ?? "fixture", -}) +}).model("gemini-2.5-flash") // Gemini does implicit prefix caching on 2.5+ models above ~1024 tokens. The // `CacheHint` is currently a no-op for Gemini (the explicit `CachedContent` diff --git a/packages/llm/test/provider/gemini.test.ts b/packages/llm/test/provider/gemini.test.ts index 7e6bbc8466d..9e519723f17 100644 --- a/packages/llm/test/provider/gemini.test.ts +++ b/packages/llm/test/provider/gemini.test.ts @@ -1,17 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" -import { LLMClient } from "../../src/route" +import { Auth, LLMClient } from "../../src/route" import * as Gemini from "../../src/protocols/gemini" import { it } from "../lib/effect" import { fixedResponse } from "../lib/http" import { sseEvents, sseRaw } from "../lib/sse" -const model = Gemini.model({ - id: "gemini-2.5-flash", - baseURL: "https://generativelanguage.test/v1beta/", - headers: { "x-goog-api-key": "test" }, -}) +const model = Gemini.route + .with({ + endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, + auth: Auth.header("x-goog-api-key", "test"), + }) + .model({ id: "gemini-2.5-flash" }) const request = LLM.request({ id: "req_1", diff --git a/packages/llm/test/provider/golden.recorded.test.ts b/packages/llm/test/provider/golden.recorded.test.ts index 3fa27c706e9..79d8589cf8c 100644 --- a/packages/llm/test/provider/golden.recorded.test.ts +++ b/packages/llm/test/provider/golden.recorded.test.ts @@ -1,32 +1,30 @@ import { Redactor } from "@opencode-ai/http-recorder" -import * as AnthropicMessages from "../../src/protocols/anthropic-messages" -import * as Gemini from "../../src/protocols/gemini" -import * as OpenAIChat from "../../src/protocols/openai-chat" -import * as OpenAIResponses from "../../src/protocols/openai-responses" -import * as Cloudflare from "../../src/providers/cloudflare" +import * as Anthropic from "../../src/providers/anthropic" +import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare" +import * as Google from "../../src/providers/google" import * as OpenAI from "../../src/providers/openai" import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenRouter from "../../src/providers/openrouter" import * as XAI from "../../src/providers/xai" import { describeRecordedGoldenScenarios } from "../recorded-golden" -const openAIChat = OpenAIChat.model({ id: "gpt-4o-mini", apiKey: process.env.OPENAI_API_KEY ?? "fixture" }) -const openAIResponses = OpenAIResponses.model({ id: "gpt-5.5", apiKey: process.env.OPENAI_API_KEY ?? "fixture" }) -const openAIResponsesWebSocket = OpenAI.responsesWebSocket("gpt-4.1-mini", { +const openAI = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY ?? "fixture", }) -const anthropicHaiku = AnthropicMessages.model({ - id: "claude-haiku-4-5-20251001", +const openAIChat = openAI.chat("gpt-4o-mini") +const openAIResponses = openAI.responses("gpt-5.5") +const openAIResponsesWebSocket = openAI.responsesWebSocket("gpt-4.1-mini") +const anthropic = Anthropic.configure({ apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture", }) -const anthropicOpus = AnthropicMessages.model({ - id: "claude-opus-4-7", - apiKey: process.env.ANTHROPIC_API_KEY ?? "fixture", -}) -const gemini = Gemini.model({ id: "gemini-2.5-flash", apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture" }) -const xaiBasic = XAI.model("grok-3-mini", { apiKey: process.env.XAI_API_KEY ?? "fixture" }) -const xaiFlagship = XAI.model("grok-4.3", { apiKey: process.env.XAI_API_KEY ?? "fixture" }) -const cloudflareAIGatewayWorkers = Cloudflare.aiGateway("workers-ai/@cf/meta/llama-3.1-8b-instruct", { +const anthropicHaiku = anthropic.model("claude-haiku-4-5-20251001") +const anthropicOpus = anthropic.model("claude-opus-4-7") +const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture" }) +const gemini = google.model("gemini-2.5-flash") +const xai = XAI.configure({ apiKey: process.env.XAI_API_KEY ?? "fixture" }) +const xaiBasic = xai.model("grok-3-mini") +const xaiFlagship = xai.model("grok-4.3") +const cloudflareAIGateway = CloudflareAIGateway.configure({ accountId: process.env.CLOUDFLARE_ACCOUNT_ID ?? "fixture-account", gatewayId: process.env.CLOUDFLARE_GATEWAY_ID && process.env.CLOUDFLARE_GATEWAY_ID !== process.env.CLOUDFLARE_ACCOUNT_ID @@ -34,32 +32,31 @@ const cloudflareAIGatewayWorkers = Cloudflare.aiGateway("workers-ai/@cf/meta/lla : undefined, gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN ?? "fixture", }) -const cloudflareAIGatewayWorkersTools = Cloudflare.aiGateway("workers-ai/@cf/openai/gpt-oss-20b", { - accountId: process.env.CLOUDFLARE_ACCOUNT_ID ?? "fixture-account", - gatewayId: - process.env.CLOUDFLARE_GATEWAY_ID && process.env.CLOUDFLARE_GATEWAY_ID !== process.env.CLOUDFLARE_ACCOUNT_ID - ? process.env.CLOUDFLARE_GATEWAY_ID - : undefined, - gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN ?? "fixture", -}) -const cloudflareWorkersAI = Cloudflare.workersAI("@cf/meta/llama-3.1-8b-instruct", { +const cloudflareWorkers = CloudflareWorkersAI.configure({ accountId: process.env.CLOUDFLARE_ACCOUNT_ID ?? "fixture-account", apiKey: process.env.CLOUDFLARE_API_KEY ?? "fixture", }) -const cloudflareWorkersAITools = Cloudflare.workersAI("@cf/openai/gpt-oss-20b", { - accountId: process.env.CLOUDFLARE_ACCOUNT_ID ?? "fixture-account", - apiKey: process.env.CLOUDFLARE_API_KEY ?? "fixture", -}) -const deepseek = OpenAICompatible.deepseek.model("deepseek-chat", { apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture" }) -const together = OpenAICompatible.togetherai.model("meta-llama/Llama-3.3-70B-Instruct-Turbo", { - apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture", -}) -const groq = OpenAICompatible.groq.model("llama-3.3-70b-versatile", { apiKey: process.env.GROQ_API_KEY ?? "fixture" }) -const openrouter = OpenRouter.model("openai/gpt-4o-mini", { apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" }) -const openrouterGpt55 = OpenRouter.model("openai/gpt-5.5", { apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" }) -const openrouterOpus = OpenRouter.model("anthropic/claude-opus-4.7", { +const cloudflareAIGatewayWorkers = cloudflareAIGateway.model("workers-ai/@cf/meta/llama-3.1-8b-instruct") +const cloudflareAIGatewayWorkersTools = cloudflareAIGateway.model("workers-ai/@cf/openai/gpt-oss-20b") +const cloudflareWorkersAI = cloudflareWorkers.model("@cf/meta/llama-3.1-8b-instruct") +const cloudflareWorkersAITools = cloudflareWorkers.model("@cf/openai/gpt-oss-20b") +const deepseek = OpenAICompatible.deepseek + .configure({ apiKey: process.env.DEEPSEEK_API_KEY ?? "fixture" }) + .model("deepseek-chat") +const together = OpenAICompatible.togetherai + .configure({ + apiKey: process.env.TOGETHER_AI_API_KEY ?? "fixture", + }) + .model("meta-llama/Llama-3.3-70B-Instruct-Turbo") +const groq = OpenAICompatible.groq + .configure({ apiKey: process.env.GROQ_API_KEY ?? "fixture" }) + .model("llama-3.3-70b-versatile") +const openRouter = OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" }) +const openrouter = openRouter.model("openai/gpt-4o-mini") +const openrouterGpt55 = openRouter.model("openai/gpt-5.5") +const openrouterOpus = OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture", -}) +}).model("anthropic/claude-opus-4.7") const redactCloudflareURL = (url: string) => url @@ -86,8 +83,11 @@ describeRecordedGoldenScenarios([ tags: ["flagship"], scenarios: [ { id: "text", temperature: false }, + { id: "reasoning", temperature: false }, + { id: "reasoning-continuation", temperature: false }, { id: "tool-call", temperature: false }, { id: "tool-loop", temperature: false }, + { id: "image-tool-result", temperature: false, maxTokens: 40 }, ], }, { @@ -113,14 +113,17 @@ describeRecordedGoldenScenarios([ requires: ["ANTHROPIC_API_KEY"], tags: ["flagship"], options: { redactor: Redactor.defaults({ requestHeaders: { allow: ["content-type", "anthropic-version"] } }) }, - scenarios: [{ id: "tool-loop", temperature: false }], + scenarios: [ + { id: "tool-loop", temperature: false }, + { id: "image-tool-result", temperature: false, maxTokens: 40 }, + ], }, { name: "Gemini 2.5 Flash", prefix: "gemini", model: gemini, requires: ["GOOGLE_GENERATIVE_AI_API_KEY"], - scenarios: [{ id: "text", maxTokens: 80 }, "tool-call"], + scenarios: [{ id: "text", maxTokens: 80 }, "tool-call", { id: "image", maxTokens: 160 }], }, { name: "xAI Grok 3 Mini", diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts index 649503943d2..71e6c581d28 100644 --- a/packages/llm/test/provider/openai-chat.test.ts +++ b/packages/llm/test/provider/openai-chat.test.ts @@ -1,11 +1,11 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { LLM, LLMError, Message, Model, ToolCallPart, Usage } from "../../src" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" import * as OpenAIChat from "../../src/protocols/openai-chat" -import { LLMClient } from "../../src/route" +import { Auth, LLMClient } from "../../src/route" import { it } from "../lib/effect" import { dynamicResponse, fixedResponse, truncatedStream } from "../lib/http" import { deltaChunk, usageChunk } from "../lib/openai-chunks" @@ -15,11 +15,14 @@ const TargetJson = Schema.fromJsonString(Schema.Unknown) const encodeJson = Schema.encodeSync(TargetJson) const decodeJson = Schema.decodeUnknownSync(TargetJson) -const model = OpenAIChat.model({ - id: "gpt-4o-mini", - baseURL: "https://api.openai.test/v1/", - headers: { authorization: "Bearer test" }, -}) +const model = OpenAIChat.route + .with({ + endpoint: { + baseURL: "https://api.openai.test/v1/", + }, + auth: Auth.bearer("test"), + }) + .model({ id: "gpt-4o-mini" }) const request = LLM.request({ id: "req_1", @@ -56,10 +59,7 @@ describe("OpenAI Chat route", () => { Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: OpenAI.chat("gpt-4o-mini", { - baseURL: "https://api.openai.test/v1/", - apiKey: "fixture", // kilocode_change - }), + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"), prompt: "think", providerOptions: { openai: { reasoningEffort: "low" } }, }), @@ -72,7 +72,9 @@ describe("OpenAI Chat route", () => { it.effect("adds native query params to the Chat Completions URL", () => LLMClient.generate( - LLM.updateRequest(request, { model: OpenAIChat.model({ ...model, queryParams: { "api-version": "v1" } }) }), + LLM.updateRequest(request, { + model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), + }), ).pipe( Effect.provide( dynamicResponse((input) => @@ -91,17 +93,18 @@ describe("OpenAI Chat route", () => { it.effect("uses Azure api-key header for static OpenAI Chat keys", () => LLMClient.generate( LLM.updateRequest(request, { - model: Azure.chat("gpt-4o-mini", { + model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", headers: { authorization: "Bearer stale" }, - }), + }).chat("gpt-4o-mini"), }), ).pipe( Effect.provide( dynamicResponse((input) => Effect.gen(function* () { const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(web.url).toBe("https://opencode-test.openai.azure.com/openai/v1/chat/completions?api-version=v1") expect(web.headers.get("api-key")).toBe("azure-key") expect(web.headers.get("authorization")).toBeNull() return input.respond(sseEvents(deltaChunk({}, "stop")), { @@ -116,7 +119,9 @@ describe("OpenAI Chat route", () => { it.effect("applies serializable HTTP overlays after payload lowering", () => LLMClient.generate( LLM.updateRequest(request, { - model: OpenAIChat.model({ ...model, apiKey: "fresh-key", headers: { authorization: "Bearer stale" } }), + model: model.route + .with({ auth: Auth.bearer("fresh-key"), headers: { authorization: "Bearer stale" } }) + .model({ id: model.id }), http: { body: { metadata: { source: "test" } }, headers: { authorization: "Bearer request", "x-custom": "yes" }, @@ -260,6 +265,32 @@ describe("OpenAI Chat route", () => { }), ) + it.effect("parses OpenAI-compatible reasoning content deltas", () => + Effect.gen(function* () { + const body = sseEvents( + { choices: [{ delta: { reasoning_content: "thinking" } }] }, + { choices: [{ delta: { content: "Hello" } }] }, + { choices: [{ delta: {}, finish_reason: "stop" }] }, + ) + + const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + + expect(response.reasoning).toBe("thinking") + expect(response.text).toBe("Hello") + expect(response.events).toMatchObject([ + { type: "step-start", index: 0 }, + { type: "reasoning-start", id: "reasoning-0" }, + { type: "reasoning-delta", id: "reasoning-0", text: "thinking" }, + { type: "text-start", id: "text-0" }, + { type: "text-delta", id: "text-0", text: "Hello" }, + { type: "reasoning-end", id: "reasoning-0" }, + { type: "text-end", id: "text-0" }, + { type: "step-finish", index: 0, reason: "stop" }, + { type: "finish", reason: "stop" }, + ]) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = sseEvents( diff --git a/packages/llm/test/provider/openai-compatible-chat.test.ts b/packages/llm/test/provider/openai-compatible-chat.test.ts index 50aac410914..43ae283e9f7 100644 --- a/packages/llm/test/provider/openai-compatible-chat.test.ts +++ b/packages/llm/test/provider/openai-compatible-chat.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { HttpClientRequest } from "effect/unstable/http" import { LLM, Message, ToolCallPart } from "../../src" -import { LLMClient } from "../../src/route" +import { Auth, LLMClient } from "../../src/route" import * as OpenAICompatible from "../../src/providers/openai-compatible" import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat" import { it } from "../lib/effect" @@ -12,13 +12,13 @@ import { sseEvents } from "../lib/sse" const Json = Schema.fromJsonString(Schema.Unknown) const decodeJson = Schema.decodeUnknownSync(Json) -const model = OpenAICompatibleChat.model({ - id: "deepseek-chat", - provider: "deepseek", - baseURL: "https://api.deepseek.test/v1/", - apiKey: "test-key", - queryParams: { "api-version": "2026-01-01" }, -}) +const model = OpenAICompatibleChat.route + .with({ + provider: "deepseek", + endpoint: { baseURL: "https://api.deepseek.test/v1/", query: { "api-version": "2026-01-01" } }, + auth: Auth.bearer("test-key"), + }) + .model({ id: "deepseek-chat" }) const request = LLM.request({ id: "req_1", @@ -63,10 +63,11 @@ describe("OpenAI-compatible Chat route", () => { expect(prepared.model).toMatchObject({ id: "deepseek-chat", provider: "deepseek", - route: "openai-compatible-chat", + route: { id: "openai-compatible-chat" }, + }) + expect(prepared.model.route.endpoint).toMatchObject({ baseURL: "https://api.deepseek.test/v1/", - apiKey: "test-key", - queryParams: { "api-version": "2026-01-01" }, + query: { "api-version": "2026-01-01" }, }) expect(prepared.body).toEqual({ model: "deepseek-chat", @@ -93,13 +94,12 @@ describe("OpenAI-compatible Chat route", () => { Effect.gen(function* () { expect( providerFamilies.map(([provider, family]) => { - const model = family.model(`${provider}-model`, { apiKey: "test-key" }) + const model = family.configure({ apiKey: "test-key" }).model(`${provider}-model`) return { id: String(model.id), provider: String(model.provider), - route: model.route, - baseURL: model.baseURL, - apiKey: model.apiKey, + route: model.route.id, + baseURL: model.route.endpoint.baseURL, } }), ).toEqual( @@ -108,19 +108,20 @@ describe("OpenAI-compatible Chat route", () => { provider, route: "openai-compatible-chat", baseURL, - apiKey: "test-key", })), ) - const custom = OpenAICompatible.deepseek.model("deepseek-chat", { - apiKey: "test-key", - baseURL: "https://custom.deepseek.test/v1", - }) + const custom = OpenAICompatible.deepseek + .configure({ + apiKey: "test-key", + baseURL: "https://custom.deepseek.test/v1", + }) + .model("deepseek-chat") expect(custom).toMatchObject({ provider: "deepseek", - route: "openai-compatible-chat", - baseURL: "https://custom.deepseek.test/v1", + route: { id: "openai-compatible-chat" }, }) + expect(custom.route.endpoint.baseURL).toBe("https://custom.deepseek.test/v1") }), ) diff --git a/packages/llm/test/provider/openai-responses-cache.recorded.test.ts b/packages/llm/test/provider/openai-responses-cache.recorded.test.ts index 2b67a0a4f2d..638c30e6671 100644 --- a/packages/llm/test/provider/openai-responses-cache.recorded.test.ts +++ b/packages/llm/test/provider/openai-responses-cache.recorded.test.ts @@ -2,14 +2,13 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { LLM } from "../../src" import { LLMClient } from "../../src/route" -import * as OpenAIResponses from "../../src/protocols/openai-responses" +import * as OpenAI from "../../src/providers/openai" import { LARGE_CACHEABLE_SYSTEM } from "../recorded-scenarios" import { recordedTests } from "../recorded-test" -const model = OpenAIResponses.model({ - id: "gpt-4.1-mini", +const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY ?? "fixture", -}) +}).responses("gpt-4.1-mini") // OpenAI caches prefixes automatically once they cross the 1024-token threshold; // `CacheHint` is a no-op for the wire body. The stable signal is the diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index 8430689531b..dd57d00a917 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -1,21 +1,20 @@ import { describe, expect } from "bun:test" import { ConfigProvider, Effect, Layer, Stream } from "effect" import { Headers, HttpClientRequest } from "effect/unstable/http" -import { LLM, LLMError, Message, ToolCallPart, Usage } from "../../src" +import { LLM, LLMError, Message, Model, ToolCallPart, Usage } from "../../src" import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route" import * as Azure from "../../src/providers/azure" import * as OpenAI from "../../src/providers/openai" import * as OpenAIResponses from "../../src/protocols/openai-responses" import * as ProviderShared from "../../src/protocols/shared" +import { continuationRequest, nativeOpenAIResponsesContinuation } from "../continuation-scenarios" import { it } from "../lib/effect" import { dynamicResponse, fixedResponse } from "../lib/http" import { sseEvents } from "../lib/sse" -const model = OpenAIResponses.model({ - id: "gpt-4.1-mini", - baseURL: "https://api.openai.test/v1/", - headers: { authorization: "Bearer test" }, -}) +const model = OpenAIResponses.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-4.1-mini" }) const request = LLM.request({ id: "req_1", @@ -27,6 +26,19 @@ const request = LLM.request({ const configEnv = (env: Record) => Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))) +type OpenAIToolOutput = Extract< + OpenAIResponses.OpenAIResponsesBody["input"][number], + { readonly type: "function_call_output" } +> + +const expectToolOutput = (body: OpenAIResponses.OpenAIResponsesBody): OpenAIToolOutput => { + const output = body.input.find( + (item): item is OpenAIToolOutput => "type" in item && item.type === "function_call_output", + ) + expect(output).toBeDefined() + return output! +} + describe("OpenAI Responses route", () => { it.effect("prepares OpenAI Responses target", () => Effect.gen(function* () { @@ -49,7 +61,9 @@ describe("OpenAI Responses route", () => { Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.updateRequest(request, { - model: OpenAI.responsesWebSocket("gpt-4.1-mini", { baseURL: "https://api.openai.test/v1/", apiKey: "test" }), + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responsesWebSocket( + "gpt-4.1-mini", + ), }), ) @@ -95,10 +109,12 @@ describe("OpenAI Responses route", () => { ) const response = yield* LLMClient.generate( LLM.request({ - model: OpenAI.responsesWebSocket("gpt-4.1-mini", { baseURL: "https://api.openai.test/v1/", apiKey: "test" }), + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responsesWebSocket( + "gpt-4.1-mini", + ), prompt: "Say hello.", }), - ).pipe(Effect.provide(LLMClient.layerWithWebSocket.pipe(Layer.provide(deps)))) + ).pipe(Effect.provide(LLMClient.layer.pipe(Layer.provide(deps)))) expect(response.text).toBe("Hi") expect(opened).toEqual([{ url: "wss://api.openai.test/v1/responses", authorization: "Bearer test" }]) @@ -113,36 +129,10 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("requires WebSocket runtime for OpenAI Responses WebSocket", () => - Effect.gen(function* () { - const error = yield* LLMClient.generate( - LLM.request({ - model: OpenAI.responsesWebSocket("gpt-4.1-mini", { baseURL: "https://api.openai.test/v1/", apiKey: "test" }), - prompt: "Say hello.", - }), - ).pipe( - Effect.provide( - LLMClient.layer.pipe( - Layer.provide( - Layer.succeed( - RequestExecutor.Service, - RequestExecutor.Service.of({ - execute: () => Effect.die("unexpected HTTP request"), - }), - ), - ), - ), - ), - Effect.flip, - ) - - expect(error.message).toContain("requires WebSocketExecutor.Service") - }), - ) - it.effect("fails immediately when WebSocket is already closed", () => Effect.gen(function* () { const error = yield* WebSocketExecutor.fromWebSocket( + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- fromWebSocket reads readyState before touching WebSocket methods on this branch. { readyState: globalThis.WebSocket.CLOSED } as globalThis.WebSocket, { url: "wss://api.openai.test/v1/responses", headers: Headers.empty }, ).pipe(Effect.flip) @@ -155,7 +145,7 @@ describe("OpenAI Responses route", () => { Effect.gen(function* () { yield* LLMClient.generate( LLM.updateRequest(request, { - model: OpenAIResponses.model({ ...model, queryParams: { "api-version": "v1" } }), + model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }), }), ).pipe( Effect.provide( @@ -177,17 +167,18 @@ describe("OpenAI Responses route", () => { Effect.gen(function* () { yield* LLMClient.generate( LLM.updateRequest(request, { - model: Azure.responses("gpt-4.1-mini", { + model: Azure.configure({ baseURL: "https://opencode-test.openai.azure.com/openai/v1/", apiKey: "azure-key", headers: { authorization: "Bearer stale" }, - }), + }).responses("gpt-4.1-mini"), }), ).pipe( Effect.provide( dynamicResponse((input) => Effect.gen(function* () { const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(web.url).toBe("https://opencode-test.openai.azure.com/openai/v1/responses?api-version=v1") expect(web.headers.get("api-key")).toBe("azure-key") expect(web.headers.get("authorization")).toBeNull() return input.respond(sseEvents({ type: "response.completed", response: {} }), { @@ -203,7 +194,7 @@ describe("OpenAI Responses route", () => { it.effect("loads OpenAI default auth from Effect Config", () => LLMClient.generate( LLM.updateRequest(request, { - model: OpenAI.responses("gpt-4.1-mini", { baseURL: "https://api.openai.test/v1/" }), + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/" }).responses("gpt-4.1-mini"), }), ).pipe( configEnv({ OPENAI_API_KEY: "env-key" }), @@ -224,10 +215,10 @@ describe("OpenAI Responses route", () => { it.effect("lets explicit auth override OpenAI default API key auth", () => LLMClient.generate( LLM.updateRequest(request, { - model: OpenAI.responses("gpt-4.1-mini", { + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", auth: Auth.bearer("oauth-token"), - }), + }).responses("gpt-4.1-mini"), }), ).pipe( Effect.provide( @@ -270,21 +261,139 @@ describe("OpenAI Responses route", () => { }), ) + // Regression: screenshot/read tool results must stay structured so base64 + // image data is not JSON-stringified into `function_call_output.output`. + it.effect("lowers image tool-result content as structured input_image items", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_image", + model, + messages: [ + Message.user("Show me the screenshot."), + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: { filePath: "shot.png" } })]), + Message.tool({ + id: "call_1", + name: "read", + resultType: "content", + result: [ + { type: "text", text: "Image read successfully" }, + { type: "media", mediaType: "image/png", data: "AAECAw==" }, + ], + }), + ], + }), + ) + + expect(expectToolOutput(prepared.body).output).toEqual([ + { type: "input_text", text: "Image read successfully" }, + { type: "input_image", image_url: "data:image/png;base64,AAECAw==" }, + ]) + }), + ) + + it.effect("lowers single-image tool-result content as structured input_image array", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_image_only", + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "screenshot", input: {} })]), + Message.tool({ + id: "call_1", + name: "screenshot", + resultType: "content", + result: [{ type: "media", mediaType: "image/png", data: "AAECAw==" }], + }), + ], + }), + ) + + expect(expectToolOutput(prepared.body).output).toEqual([ + { type: "input_image", image_url: "data:image/png;base64,AAECAw==" }, + ]) + }), + ) + + it.effect("rejects non-image media in tool-result content with a clear error", () => + Effect.gen(function* () { + const error = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_result_unsupported_media", + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "fetch", input: {} })]), + Message.tool({ + id: "call_1", + name: "fetch", + resultType: "content", + result: [{ type: "media", mediaType: "audio/mpeg", data: "AAECAw==" }], + }), + ], + }), + ).pipe(Effect.flip) + + expect(error.message).toContain("OpenAI Responses") + expect(error.message).toContain("audio/mpeg") + }), + ) + + it.effect("prepares the composed native continuation request", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + continuationRequest({ + id: "req_native_continuation_openai", + model, + features: nativeOpenAIResponsesContinuation, + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { role: "system", content: "You are concise. Continue from the provided history." }, + { + role: "user", + content: [ + { type: "input_text", text: "What is shown here?" }, + { type: "input_image", image_url: "data:image/png;base64,AAECAw==" }, + ], + }, + { + type: "reasoning", + id: "rs_continuation_1", + encrypted_content: "encrypted-continuation-state", + summary: [{ type: "summary_text", text: "I inspected the previous turn." }], + }, + { role: "assistant", content: [{ type: "output_text", text: "It shows a small test image." }] }, + { role: "user", content: [{ type: "input_text", text: "Check the weather in Paris before continuing." }] }, + { type: "function_call", call_id: "call_weather_1", name: "get_weather", arguments: '{"city":"Paris"}' }, + { type: "function_call_output", call_id: "call_weather_1", output: '{"temperature":22}' }, + { role: "assistant", content: [{ type: "output_text", text: "Paris is 22 degrees." }] }, + { + role: "user", + content: [{ type: "input_text", text: "Continue from this conversation in one short sentence." }], + }, + ], + include: ["reasoning.encrypted_content"], + store: false, + }) + expect(prepared.body.tools).toEqual([expect.objectContaining({ type: "function", name: "get_weather" })]) + }), + ) + it.effect("maps OpenAI provider options to Responses options", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: OpenAI.model("gpt-5.2", { - baseURL: "https://api.openai.test/v1/", - apiKey: "fixture", // kilocode_change - }), + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), prompt: "think", providerOptions: { openai: { promptCacheKey: "session_123", reasoningEffort: "high", reasoningSummary: "auto", - includeEncryptedReasoning: true, + include: ["reasoning.encrypted_content"], }, }, }), @@ -298,15 +407,117 @@ describe("OpenAI Responses route", () => { }), ) - it.effect("request OpenAI provider options override model defaults", () => + it.effect("accepts the full ResponseIncludable union", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: OpenAI.model("gpt-4.1-mini", { + model, + prompt: "hi", + providerOptions: { + openai: { + include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"], + }, + }, + }), + ) + + expect(prepared.body.include).toEqual([ + "reasoning.encrypted_content", + "code_interpreter_call.outputs", + "web_search_call.results", + ]) + }), + ) + + it.effect("filters unknown includable values out of the include array", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + prompt: "hi", + // The user passed one invalid entry alongside a valid one. Keep the + // valid one so the request still succeeds rather than failing on a + // typo from upstream config. + providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } }, + }), + ) + + expect(prepared.body.include).toEqual(["reasoning.encrypted_content"]) + }), + ) + + it.effect("treats an explicit empty include as no include at all", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }), + ) + + expect(prepared.body.include).toBeUndefined() + }), + ) + + it.effect("treats an all-invalid include as no include at all", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }), + ) + + expect(prepared.body.include).toBeUndefined() + }), + ) + + it.effect("omits include when no include is set", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }), + ) + + expect(prepared.body.include).toBeUndefined() + }), + ) + + it.effect("requests encrypted reasoning by default for GPT-5 reasoning models", () => + Effect.gen(function* () { + // The native OpenAI facade configures GPT-5 stateless (store: false) with + // reasoningSummary: "auto" by default. Without `include`, a follow-up + // turn cannot replay reasoning state, so the facade also opts into + // `reasoning.encrypted_content` automatically. + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"), + prompt: "hi", + }), + ) + + expect(prepared.body.store).toBe(false) + expect(prepared.body.include).toEqual(["reasoning.encrypted_content"]) + expect(prepared.body.reasoning).toEqual({ effort: "medium", summary: "auto" }) + }), + ) + + it.effect("lets callers opt out of the GPT-5 default include", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"), + prompt: "hi", + providerOptions: { openai: { include: [] } }, + }), + ) + + expect(prepared.body.include).toBeUndefined() + }), + ) + + it.effect("request OpenAI provider options override route defaults", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", - apiKey: "fixture", // kilocode_change + apiKey: "test", providerOptions: { openai: { promptCacheKey: "model_cache" } }, - }), + }).model("gpt-4.1-mini"), prompt: "no cache", providerOptions: { openai: { promptCacheKey: "request_cache" } }, }), @@ -379,6 +590,349 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("parses reasoning summary stream fixtures", () => + Effect.gen(function* () { + const body = sseEvents( + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", delta: "thinking" }, + { type: "response.output_text.delta", item_id: "msg_1", delta: "Hello" }, + { type: "response.reasoning_summary_text.done", item_id: "rs_1" }, + { type: "response.completed", response: { id: "resp_1" } }, + ) + + const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body))) + + expect(response.reasoning).toBe("thinking") + expect(response.text).toBe("Hello") + expect(response.events).toMatchObject([ + { type: "step-start", index: 0 }, + { type: "reasoning-start", id: "rs_1" }, + { type: "reasoning-delta", id: "rs_1", text: "thinking" }, + { type: "text-start", id: "msg_1" }, + { type: "text-delta", id: "msg_1", text: "Hello" }, + { type: "reasoning-end", id: "rs_1" }, + { type: "text-end", id: "msg_1" }, + { type: "step-finish", index: 0, reason: "stop" }, + { type: "finish", reason: "stop" }, + ]) + }), + ) + + it.effect("preserves encrypted reasoning metadata for continuation", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", delta: "thinking" }, + { + type: "response.output_item.done", + item: { + type: "reasoning", + id: "rs_1", + encrypted_content: "encrypted-state", + summary: [{ type: "summary_text", text: "thinking" }], + }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.events).toContainEqual( + expect.objectContaining({ + type: "reasoning-end", + id: "rs_1", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }), + ) + }), + ) + + it.effect("streams each reasoning summary part as a separate block", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.updateRequest(request, { providerOptions: { openai: { store: false } } }), + ).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.added", + item: { type: "reasoning", id: "rs_1", encrypted_content: null }, + }, + { type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 }, + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" }, + { type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 }, + { type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 }, + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" }, + { type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 }, + { + type: "response.output_item.done", + item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.reasoning).toBe("FirstSecond") + expect(response.events).toMatchObject([ + { type: "step-start", index: 0 }, + { + type: "reasoning-start", + id: "rs_1:0", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } }, + }, + { type: "reasoning-delta", id: "rs_1:0", text: "First" }, + { type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } }, + { + type: "reasoning-start", + id: "rs_1:1", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } }, + }, + { type: "reasoning-delta", id: "rs_1:1", text: "Second" }, + { + type: "reasoning-end", + id: "rs_1:1", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }, + { type: "step-finish", index: 0, reason: "stop" }, + { type: "finish", reason: "stop" }, + ]) + }), + ) + + it.effect("closes reasoning summary parts when storage is not disabled", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents( + { + type: "response.output_item.added", + item: { type: "reasoning", id: "rs_1", encrypted_content: null }, + }, + { type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 }, + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" }, + { type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 }, + { type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 }, + { type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" }, + { type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 }, + { + type: "response.output_item.done", + item: { type: "reasoning", id: "rs_1", encrypted_content: null }, + }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + ), + ), + ) + + expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([ + { type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } }, + { type: "reasoning-end", id: "rs_1:1", providerMetadata: { openai: { itemId: "rs_1" } } }, + ]) + }), + ) + + it.effect("continues a stateless reasoning conversation", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate( + LLM.request({ + id: "req_reasoning_continue", + model, + messages: [ + Message.user("What changed?"), + Message.assistant([ + { + type: "reasoning", + text: "Checked the previous diff.", + providerMetadata: { + openai: { + itemId: "rs_1", + reasoningEncryptedContent: "encrypted-state", + }, + }, + }, + { type: "text", text: "The parser changed." }, + ]), + Message.user("Summarize it."), + ], + providerOptions: { openai: { store: false } }, + }), + ).pipe( + Effect.provide( + dynamicResponse((input) => + Effect.gen(function* () { + const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie) + expect(yield* Effect.promise(() => web.json())).toMatchObject({ + input: [ + { role: "user", content: [{ type: "input_text", text: "What changed?" }] }, + { + type: "reasoning", + id: "rs_1", + encrypted_content: "encrypted-state", + summary: [{ type: "summary_text", text: "Checked the previous diff." }], + }, + { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, + { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, + ], + }) + return input.respond( + sseEvents( + { type: "response.output_text.delta", item_id: "msg_1", delta: "Parser now round-trips reasoning." }, + { type: "response.completed", response: { id: "resp_1" } }, + ), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ), + ), + ) + + expect(response.text).toBe("Parser now round-trips reasoning.") + }), + ) + + it.effect("preserves assistant content order around reasoning items", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_reasoning_order", + model, + messages: [ + Message.assistant([ + { type: "text", text: "Before." }, + { + type: "reasoning", + text: "Checked order.", + providerMetadata: { + openai: { + itemId: "rs_1", + reasoningEncryptedContent: "encrypted-state", + }, + }, + }, + { type: "text", text: "After." }, + ]), + ], + providerOptions: { openai: { store: false } }, + }), + ) + + expect(prepared.body.input).toEqual([ + { role: "assistant", content: [{ type: "output_text", text: "Before." }] }, + { + type: "reasoning", + id: "rs_1", + encrypted_content: "encrypted-state", + summary: [{ type: "summary_text", text: "Checked order." }], + }, + { role: "assistant", content: [{ type: "output_text", text: "After." }] }, + ]) + }), + ) + + it.effect("references stored reasoning items by id", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model, + messages: [ + Message.assistant([ + { + type: "reasoning", + text: "Checked the previous diff.", + providerMetadata: { openai: { itemId: "rs_1" } }, + }, + ]), + ], + providerOptions: { openai: { store: true } }, + }), + ) + + expect(prepared.body.input).toEqual([{ type: "item_reference", id: "rs_1" }]) + }), + ) + + it.effect("joins streamed summary blocks into one continuation reasoning item", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_multi_summary_continuation", + model, + messages: [ + Message.assistant([ + { + type: "reasoning", + text: "First", + providerMetadata: { openai: { itemId: "rs_1" } }, + }, + { + type: "reasoning", + text: "Second", + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + }, + ]), + ], + providerOptions: { openai: { store: false } }, + }), + ) + + expect(prepared.body.input).toEqual([ + { + type: "reasoning", + id: "rs_1", + encrypted_content: "encrypted-state", + summary: [ + { type: "summary_text", text: "First" }, + { type: "summary_text", text: "Second" }, + ], + }, + ]) + }), + ) + + it.effect("skips non-persisted reasoning ids without encrypted state", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_reasoning_without_encrypted_state", + model, + messages: [ + Message.user("What changed?"), + Message.assistant([ + { + type: "reasoning", + text: "Checked the previous diff.", + providerMetadata: { + openai: { + itemId: "rs_1", + reasoningEncryptedContent: null, + }, + }, + }, + { type: "text", text: "The parser changed." }, + ]), + Message.user("Summarize it."), + ], + providerOptions: { openai: { store: false } }, + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { role: "user", content: [{ type: "input_text", text: "What changed?" }] }, + { role: "assistant", content: [{ type: "output_text", text: "The parser changed." }] }, + { role: "user", content: [{ type: "input_text", text: "Summarize it." }] }, + ], + store: false, + }) + }), + ) + it.effect("assembles streamed function call input", () => Effect.gen(function* () { const body = sseEvents( @@ -536,17 +1090,36 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("lowers user image content", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_media", + model, + messages: [Message.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })], + }), + ) + + expect(prepared.body.input).toEqual([ + { + role: "user", + content: [{ type: "input_image", image_url: "data:image/png;base64,AAECAw==" }], + }, + ]) + }), + ) + it.effect("rejects unsupported user media content", () => Effect.gen(function* () { const error = yield* LLMClient.prepare( LLM.request({ id: "req_media", model, - messages: [Message.user({ type: "media", mediaType: "image/png", data: "AAECAw==" })], + messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "AAECAw==" })], }), ).pipe(Effect.flip) - expect(error.message).toContain("OpenAI Responses user messages only support text content for now") + expect(error.message).toContain("OpenAI Responses user media content only supports images") }), ) @@ -556,7 +1129,11 @@ describe("OpenAI Responses route", () => { Effect.provide(fixedResponse(sseEvents({ type: "error", code: "rate_limit_exceeded", message: "Slow down" }))), ) - expect(response.events).toEqual([{ type: "provider-error", message: "Slow down" }]) + // Prefix the code so consumers see the failure mode, not just the + // sometimes-generic provider message. The bare message alone meant + // production errors like rate limits were indistinguishable from + // unrelated stream failures. + expect(response.events).toEqual([{ type: "provider-error", message: "rate_limit_exceeded: Slow down" }]) }), ) @@ -570,6 +1147,99 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("falls back to error code when message is empty", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "error", code: "internal_error", message: "" }))), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "internal_error" }]) + }), + ) + + // Regression: `response.failed` carries the failure details under + // `response.error`, not at the top level. The previous handler only + // checked top-level `message`/`code` and so always emitted the bare + // "OpenAI Responses response failed" string, hiding the real cause. + it.effect("surfaces response.failed details from response.error", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "response.failed", + response: { + id: "resp_failed_1", + error: { code: "server_error", message: "Upstream model unavailable" }, + }, + }), + ), + ), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "server_error: Upstream model unavailable" }]) + }), + ) + + it.effect("surfaces response.failed code when no nested message is present", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "response.failed", + response: { id: "resp_failed_2", error: { code: "invalid_prompt" } }, + }), + ), + ), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "invalid_prompt" }]) + }), + ) + + it.effect("surfaces error event details even when they arrive nested under response.error", () => + Effect.gen(function* () { + // Some OpenAI-compatible proxies and older SDK versions wrap the + // top-level error fields into a nested `response.error` payload + // when they bubble up an HTTP error as an SSE `error` event. Honour + // both shapes so the user still sees the underlying cause instead + // of the catch-all string. + const response = yield* LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "error", + response: { error: { code: "context_length_exceeded", message: "prompt too long" } }, + }), + ), + ), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "context_length_exceeded: prompt too long" }]) + }), + ) + + it.effect("falls back to a stable default when both error and response are absent", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "error" }))), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses stream error" }]) + }), + ) + + it.effect("falls back to a stable default when response.failed has no error payload", () => + Effect.gen(function* () { + const response = yield* LLMClient.generate(request).pipe( + Effect.provide(fixedResponse(sseEvents({ type: "response.failed", response: { id: "resp_failed_3" } }))), + ) + + expect(response.events).toEqual([{ type: "provider-error", message: "OpenAI Responses response failed" }]) + }), + ) + it.effect("fails HTTP provider errors before stream parsing", () => Effect.gen(function* () { const error = yield* LLMClient.generate(request).pipe( diff --git a/packages/llm/test/provider/openrouter.test.ts b/packages/llm/test/provider/openrouter.test.ts index b3fb6bddc76..86d1317b3e6 100644 --- a/packages/llm/test/provider/openrouter.test.ts +++ b/packages/llm/test/provider/openrouter.test.ts @@ -8,15 +8,14 @@ import { it } from "../lib/effect" describe("OpenRouter", () => { it.effect("prepares OpenRouter models through the OpenAI-compatible Chat route", () => Effect.gen(function* () { - const model = OpenRouter.model("openai/gpt-4o-mini", { apiKey: "test-key" }) + const model = OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini") expect(model).toMatchObject({ id: "openai/gpt-4o-mini", provider: "openrouter", - route: "openrouter", - baseURL: "https://openrouter.ai/api/v1", - apiKey: "test-key", + route: { id: "openrouter" }, }) + expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1") const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." })) @@ -33,7 +32,8 @@ describe("OpenRouter", () => { Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - model: OpenRouter.model("anthropic/claude-3.7-sonnet:thinking", { + model: OpenRouter.configure({ + apiKey: "test-key", providerOptions: { openrouter: { usage: true, @@ -41,7 +41,7 @@ describe("OpenRouter", () => { promptCacheKey: "session_123", }, }, - }), + }).model("anthropic/claude-3.7-sonnet:thinking"), prompt: "Think briefly.", }), ) diff --git a/packages/llm/test/recorded-golden.ts b/packages/llm/test/recorded-golden.ts index 6a6c8c7ac9d..76568ca6714 100644 --- a/packages/llm/test/recorded-golden.ts +++ b/packages/llm/test/recorded-golden.ts @@ -1,8 +1,8 @@ import type { HttpRecorder } from "@opencode-ai/http-recorder" -import { describe, type TestOptions } from "bun:test" +import { describe } from "bun:test" import { Effect } from "effect" -import type { ModelRef } from "../src" -import { goldenScenarioTags, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios" +import type { Model } from "../src" +import { goldenScenarioTags, goldenScenarioTitle, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios" import { recordedTests } from "./recorded-test" import { kebab } from "./recorded-utils" @@ -17,12 +17,12 @@ type ScenarioInput = readonly tags?: ReadonlyArray readonly maxTokens?: number readonly temperature?: number | false - readonly timeout?: number | TestOptions + readonly timeout?: number } type TargetInput = { readonly name: string - readonly model: ModelRef + readonly model: Model readonly protocol?: string readonly requires?: ReadonlyArray readonly transport?: Transport @@ -35,22 +35,16 @@ type TargetInput = { const scenarioInput = (input: ScenarioInput) => (typeof input === "string" ? { id: input } : input) -const scenarioTitle = (id: GoldenScenarioID) => { - if (id === "text") return "streams text" - if (id === "tool-call") return "streams tool call" - return "drives a tool loop" -} - const defaultPrefix = (target: TargetInput) => { if (target.prefix) return target.prefix const transport = target.transport === "websocket" ? "-websocket" : "" - return `${target.model.provider}-${target.protocol ?? target.model.route}${transport}` + return `${target.model.provider}-${target.protocol ?? target.model.route.id}${transport}` } const metadata = (target: TargetInput) => ({ provider: target.model.provider, protocol: target.protocol, - route: target.model.route, + route: target.model.route.id, transport: target.transport ?? "http", model: target.model.id, ...target.metadata, @@ -75,7 +69,7 @@ const runTarget = (target: TargetInput) => { describe(`${target.name} recorded`, () => { target.scenarios.forEach((raw) => { const input = scenarioInput(raw) - const name = input.name ?? scenarioTitle(input.id) + const name = input.name ?? goldenScenarioTitle(input.id) recorded.effect.with( name, { diff --git a/packages/llm/test/recorded-scenarios.ts b/packages/llm/test/recorded-scenarios.ts index 3af7a776088..db28ec4493f 100644 --- a/packages/llm/test/recorded-scenarios.ts +++ b/packages/llm/test/recorded-scenarios.ts @@ -1,6 +1,17 @@ import { expect } from "bun:test" import { Effect, Schema, Stream } from "effect" -import { LLM, LLMEvent, LLMResponse, ToolChoice, ToolDefinition, type LLMRequest, type ModelRef } from "../src" +import { + LLM, + LLMEvent, + LLMResponse, + Message, + ToolChoice, + ToolDefinition, + type ContentPart, + type FinishReason, + type LLMRequest, + type Model, +} from "../src" import { LLMClient } from "../src/route" import { tool } from "../src/tool" @@ -39,48 +50,9 @@ export const weatherRuntimeTool = tool({ ), }) -export const textRequest = (input: { - readonly id: string - readonly model: ModelRef - readonly prompt?: string - readonly maxTokens?: number - readonly temperature?: number | false -}) => - LLM.request({ - id: input.id, - model: input.model, - system: "You are concise.", - prompt: input.prompt ?? "Reply with exactly: Hello!", - cache: "none", - generation: - input.temperature === false - ? { maxTokens: input.maxTokens ?? 20 } - : { maxTokens: input.maxTokens ?? 20, temperature: input.temperature ?? 0 }, - }) - -export const weatherToolRequest = (input: { - readonly id: string - readonly model: ModelRef - readonly maxTokens?: number - readonly temperature?: number | false -}) => - LLM.request({ - id: input.id, - model: input.model, - system: "Call tools exactly as requested.", - prompt: "Call get_weather with city exactly Paris.", - tools: [weatherTool], - toolChoice: ToolChoice.make(weatherTool), - cache: "none", - generation: - input.temperature === false - ? { maxTokens: input.maxTokens ?? 80 } - : { maxTokens: input.maxTokens ?? 80, temperature: input.temperature ?? 0 }, - }) - export const weatherToolLoopRequest = (input: { readonly id: string - readonly model: ModelRef + readonly model: Model readonly system?: string readonly maxTokens?: number readonly temperature?: number | false @@ -99,7 +71,7 @@ export const weatherToolLoopRequest = (input: { export const goldenWeatherToolLoopRequest = (input: { readonly id: string - readonly model: ModelRef + readonly model: Model readonly maxTokens?: number readonly temperature?: number | false }) => @@ -108,6 +80,12 @@ export const goldenWeatherToolLoopRequest = (input: { system: "Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.", }) +const RESTROOM_IMAGE_TEXT = "jiggling restroom prison" +const restroomImage = () => + Effect.promise(() => Bun.file(new URL("./fixtures/media/restroom.png", import.meta.url)).bytes()).pipe( + Effect.map((bytes) => Buffer.from(bytes).toString("base64")), + ) + export const runWeatherToolLoop = (request: LLMRequest) => LLMClient.stream({ request, @@ -158,54 +136,253 @@ export const expectGoldenWeatherToolLoop = (events: ReadonlyArray) => expect(LLMResponse.text({ events }).trim()).toMatch(/^Paris is sunny\.?$/) } -export type GoldenScenarioID = "text" | "tool-call" | "tool-loop" - export interface GoldenScenarioContext { readonly id: string - readonly model: ModelRef + readonly model: Model readonly maxTokens?: number readonly temperature?: number | false } const generate = (request: LLMRequest) => LLMClient.generate(request) -export const goldenScenarioTags = (id: GoldenScenarioID) => { - if (id === "text") return ["text", "golden"] - if (id === "tool-call") return ["tool", "tool-call", "golden"] - return ["tool", "tool-loop", "golden"] +const generation = (context: GoldenScenarioContext, maxTokens: number) => + context.temperature === false ? { maxTokens } : { maxTokens, temperature: context.temperature ?? 0 } + +const normalizeImageText = (value: string) => + value + .toLowerCase() + .replace(/[^a-z\s]/g, "") + .replace(/\s+/g, " ") + .trim() + +const encryptedReasoningOptions = { + openai: { + store: false, + include: ["reasoning.encrypted_content"], + reasoningEffort: "low", + reasoningSummary: "auto", + }, +} as const + +type AssistantTextExpectation = string | RegExp + +type UserStep = { readonly type: "user"; readonly content: Message.ContentInput } +type AssistantStep = { + readonly type: "assistant" + readonly text?: AssistantTextExpectation + readonly toolCall?: { readonly name: string; readonly input: unknown } + readonly reasoning?: "openai-encrypted" + readonly id?: string + readonly system?: string + readonly maxTokens?: number + readonly finish?: FinishReason + readonly tools?: LLM.RequestInput["tools"] + readonly toolChoice?: LLM.RequestInput["toolChoice"] + readonly providerOptions?: LLMRequest["providerOptions"] + readonly assert?: (response: LLMResponse) => void +} +type ConversationStep = UserStep | AssistantStep + +const user = (content: Message.ContentInput): ConversationStep => ({ type: "user", content }) + +const assistant = { + expectText: ( + text: AssistantTextExpectation, + options?: Omit, + ): ConversationStep => ({ type: "assistant", text, ...options }), + expectToolCall: ( + name: string, + input: unknown, + options?: Omit, + ): ConversationStep => ({ type: "assistant", toolCall: { name, input }, finish: "tool-calls", ...options }), + expectEncryptedReasoningText: ( + text: AssistantTextExpectation, + options?: Omit, + ): ConversationStep => ({ + type: "assistant", + text, + reasoning: "openai-encrypted", + providerOptions: encryptedReasoningOptions, + ...options, + }), } -export const runGoldenScenario = (id: GoldenScenarioID, context: GoldenScenarioContext) => +const assertAssistantText = (actual: string, expected: AssistantTextExpectation) => { + if (typeof expected === "string") { + expect(actual.trim()).toBe(expected) + return + } + expect(actual.trim()).toMatch(expected) +} + +const assertAssistantToolCall = (response: LLMResponse, expected: NonNullable) => { + expect(response.toolCalls).toMatchObject([ + { type: "tool-call", id: expect.any(String), name: expected.name, input: expected.input }, + ]) +} + +// The generated golden scenarios only model one assistant shape at a time: +// encrypted reasoning + text, text, or tool call. Keep mixed interleavings in +// focused protocol tests where event order can be asserted directly. +const assistantMessageFromResponse = (response: LLMResponse, step: AssistantStep) => { + const content: ContentPart[] = [] + if (step.reasoning === "openai-encrypted") { + const reasoning = response.events.find( + (event): event is Extract => + LLMEvent.is.reasoningEnd(event) && typeof event.providerMetadata?.openai?.itemId === "string", + ) + if (!reasoning) throw new Error("OpenAI Responses did not return reasoning metadata") + expect(reasoning.providerMetadata?.openai?.reasoningEncryptedContent).toEqual(expect.any(String)) + content.push({ type: "reasoning", text: response.reasoning, providerMetadata: reasoning.providerMetadata }) + } + + if (response.text.length > 0) content.push({ type: "text", text: response.text }) + content.push(...response.toolCalls) + return Message.assistant(content) +} + +const runGeneratedConversation = (context: GoldenScenarioContext, steps: ReadonlyArray) => Effect.gen(function* () { - if (id === "text") { + const messages: Message[] = [] + let generated = 0 + for (const step of steps) { + if (step.type === "user") { + messages.push(Message.user(step.content)) + continue + } + + generated += 1 const response = yield* generate( - textRequest({ - id: context.id, + LLM.request({ + id: step.id ? `${context.id}_${step.id}` : `${context.id}_${generated}`, model: context.model, - prompt: "Reply exactly with: Hello!", - maxTokens: context.maxTokens ?? 40, - temperature: context.temperature, + system: step.system, + cache: "none", + messages, + tools: step.tools, + toolChoice: step.toolChoice, + providerOptions: step.providerOptions, + generation: generation(context, step.maxTokens ?? context.maxTokens ?? 80), }), ) - expect(response.text.trim()).toMatch(/^Hello!?$/) - expectFinish(response.events, "stop") - return + if (step.text !== undefined) assertAssistantText(response.text, step.text) + if (step.toolCall) assertAssistantToolCall(response, step.toolCall) + step.assert?.(response) + expectFinish(response.events, step.finish ?? "stop") + messages.push(assistantMessageFromResponse(response, step)) } + }) - if (id === "tool-call") { - const response = yield* generate( - weatherToolRequest({ - id: context.id, - model: context.model, - maxTokens: context.maxTokens ?? 80, - temperature: context.temperature, - }), - ) - expectWeatherToolCall(response) - expectFinish(response.events, "tool-calls") - return - } +const runTextScenario = (context: GoldenScenarioContext) => + runGeneratedConversation(context, [ + user("Reply exactly with: Hello!"), + assistant.expectText(/^Hello!?$/, { + system: "You are concise.", + maxTokens: context.maxTokens ?? 40, + providerOptions: + context.model.route.id === "gemini" ? { gemini: { thinkingConfig: { thinkingBudget: 0 } } } : undefined, + }), + ]) +const runToolCallScenario = (context: GoldenScenarioContext) => + runGeneratedConversation(context, [ + user("Call get_weather with city exactly Paris."), + assistant.expectToolCall( + weatherToolName, + { city: "Paris" }, + { + system: "Call tools exactly as requested.", + tools: [weatherTool], + toolChoice: ToolChoice.make(weatherTool), + maxTokens: context.maxTokens ?? 80, + }, + ), + ]) + +const runImageScenario = (context: GoldenScenarioContext) => + Effect.gen(function* () { + yield* runGeneratedConversation(context, [ + user([ + { + type: "text", + text: "The image contains exactly three lowercase English words. Read them left to right and reply with only those words.", + }, + { type: "media", mediaType: "image/png", data: yield* restroomImage() }, + ]), + assistant.expectText(/.+/, { + system: "Read images carefully. Reply only with the visible text.", + maxTokens: context.maxTokens ?? 20, + assert: (response) => expect(normalizeImageText(response.text)).toBe(RESTROOM_IMAGE_TEXT), + }), + ]) + }) + +// Reproduces a tool-result image round trip: a tool returns image bytes, and +// the next model turn must receive provider-native image content instead of a +// JSON-stringified base64 blob. +const screenshotToolName = "read_screenshot" +const runImageToolResultScenario = (context: GoldenScenarioContext) => + Effect.gen(function* () { + const image = yield* restroomImage() + const response = yield* generate( + LLM.request({ + id: `${context.id}_image_tool_result`, + model: context.model, + system: "Read images carefully. Reply only with the visible text, lowercase, no punctuation.", + cache: "none", + generation: generation(context, context.maxTokens ?? 40), + messages: [ + Message.user("Use the read_screenshot tool, then reply with the words shown."), + Message.assistant([{ type: "tool-call", id: "call_screenshot_1", name: screenshotToolName, input: {} }]), + Message.tool({ + id: "call_screenshot_1", + name: screenshotToolName, + resultType: "content", + result: [ + { type: "text", text: "Image read successfully" }, + { type: "media", mediaType: "image/png", data: image }, + ], + }), + ], + tools: [ + ToolDefinition.make({ + name: screenshotToolName, + description: "Capture a screenshot of the current screen.", + inputSchema: { type: "object", properties: {}, additionalProperties: false }, + }), + ], + }), + ) + + expectFinish(response.events, "stop") + expect(normalizeImageText(response.text)).toBe(RESTROOM_IMAGE_TEXT) + }) + +const runReasoningScenario = (context: GoldenScenarioContext) => + runGeneratedConversation(context, [ + user("Think briefly, then reply exactly with: Hello!"), + assistant.expectText(/^Hello!?$/, { + system: "Show concise reasoning when the provider supports visible reasoning summaries.", + providerOptions: { openai: { reasoningEffort: "low", reasoningSummary: "auto" } }, + maxTokens: context.maxTokens ?? 120, + assert: (response) => expect(response.usage?.reasoningTokens ?? 0).toBeGreaterThan(0), + }), + ]) + +const runReasoningContinuationScenario = (context: GoldenScenarioContext) => + runGeneratedConversation(context, [ + user("Think briefly, then reply exactly with: Hello!"), + assistant.expectEncryptedReasoningText(/^Hello!?$/, { + id: "first", + system: "Show concise reasoning when the provider supports visible reasoning summaries.", + maxTokens: context.maxTokens ?? 120, + }), + user("Now reply exactly with: Done."), + assistant.expectText(/^Done\.?$/, { id: "second", maxTokens: 40, providerOptions: encryptedReasoningOptions }), + ]) + +const runToolLoopScenario = (context: GoldenScenarioContext) => + Effect.gen(function* () { expectGoldenWeatherToolLoop( yield* runWeatherToolLoop( goldenWeatherToolLoopRequest({ @@ -218,6 +395,30 @@ export const runGoldenScenario = (id: GoldenScenarioID, context: GoldenScenarioC ) }) +const goldenScenarios = { + text: { title: "streams text", tags: ["text", "golden"], run: runTextScenario }, + "tool-call": { title: "streams tool call", tags: ["tool", "tool-call", "golden"], run: runToolCallScenario }, + "tool-loop": { title: "drives a tool loop", tags: ["tool", "tool-loop", "golden"], run: runToolLoopScenario }, + image: { title: "reads image text", tags: ["media", "image", "vision", "golden"], run: runImageScenario }, + "image-tool-result": { + title: "reads image returned from tool result", + tags: ["media", "image", "vision", "tool", "tool-result", "golden"], + run: runImageToolResultScenario, + }, + reasoning: { title: "uses reasoning", tags: ["reasoning", "golden"], run: runReasoningScenario }, + "reasoning-continuation": { + title: "continues encrypted reasoning", + tags: ["reasoning", "continuation", "encrypted-reasoning", "golden"], + run: runReasoningContinuationScenario, + }, +} as const + +export type GoldenScenarioID = keyof typeof goldenScenarios +export const goldenScenarioTitle = (id: GoldenScenarioID) => goldenScenarios[id].title +export const goldenScenarioTags = (id: GoldenScenarioID) => [...goldenScenarios[id].tags] +export const runGoldenScenario = (id: GoldenScenarioID, context: GoldenScenarioContext) => + goldenScenarios[id].run(context) + const usageSummary = (usage: LLMResponse["usage"] | undefined) => { if (!usage) return undefined return Object.fromEntries( @@ -235,7 +436,7 @@ const usageSummary = (usage: LLMResponse["usage"] | undefined) => { const pushText = (summary: Array>, type: "text" | "reasoning", value: string) => { const last = summary.at(-1) if (last?.type === type) { - last.value = `${last.value ?? ""}${value}` + last.value = `${typeof last.value === "string" ? last.value : ""}${value}` return } summary.push({ type, value }) diff --git a/packages/llm/test/recorded-test.ts b/packages/llm/test/recorded-test.ts index 62e51337d93..bbc8b93da69 100644 --- a/packages/llm/test/recorded-test.ts +++ b/packages/llm/test/recorded-test.ts @@ -69,8 +69,6 @@ export const recordedTests = (options: RecordedTestsOptions) => requestExecutor, webSocketCassetteLayer(cassette, { metadata: recorderMetadata, mode }), ) - return Layer.mergeAll(deps, LLMClient.layerWithWebSocket.pipe(Layer.provide(deps))).pipe( - Layer.provide(cassetteService), - ) + return Layer.mergeAll(deps, LLMClient.layer.pipe(Layer.provide(deps))).pipe(Layer.provide(cassetteService)) }, }) diff --git a/packages/llm/test/route.test.ts b/packages/llm/test/route.test.ts new file mode 100644 index 00000000000..681583bc9e4 --- /dev/null +++ b/packages/llm/test/route.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test" +import * as OpenAIChat from "../src/protocols/openai-chat" +import { Auth } from "../src/route" + +describe("Route.with", () => { + test("merges endpoint query and header defaults while replacing auth and id", () => { + const auth = Auth.headers({ "x-auth": "new" }) + const route = OpenAIChat.route + .with({ + id: "base-chat", + endpoint: { + baseURL: "https://api.example.test/v1", + query: { keep: "base", base: "1" }, + }, + headers: { "x-base": "base", "x-override": "base" }, + auth: Auth.headers({ "x-auth": "old" }), + }) + .with({ + id: "patched-chat", + endpoint: { query: { keep: "patch", patch: "1" } }, + headers: { "x-override": "patch", "x-patch": "patch" }, + auth, + }) + + expect(route.id).toBe("patched-chat") + expect(route.auth).toBe(auth) + expect(route.endpoint).toMatchObject({ + baseURL: "https://api.example.test/v1", + path: "/chat/completions", + query: { keep: "patch", base: "1", patch: "1" }, + }) + expect(route.defaults.headers).toEqual({ + "x-base": "base", + "x-override": "patch", + "x-patch": "patch", + }) + expect(route.defaults.http?.headers).toEqual({ + "x-base": "base", + "x-override": "patch", + "x-patch": "patch", + }) + }) +}) diff --git a/packages/llm/test/schema.test.ts b/packages/llm/test/schema.test.ts index 01d6fadd9f5..3c6628c2e51 100644 --- a/packages/llm/test/schema.test.ts +++ b/packages/llm/test/schema.test.ts @@ -1,16 +1,19 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { ContentPart, LLMEvent, LLMRequest, ModelID, ModelLimits, ModelRef, ProviderID, Usage } from "../src/schema" +import * as OpenAIChat from "../src/protocols/openai-chat" +import * as OpenAIResponses from "../src/protocols/openai-responses" +import { ContentPart, LLMEvent, LLMRequest, Model, ModelID, ProviderID, Usage } from "../src/schema" import { ProviderShared } from "../src/protocols/shared" -const model = new ModelRef({ +const model = new Model({ id: ModelID.make("fake-model"), provider: ProviderID.make("fake-provider"), - route: "openai-chat", - baseURL: "https://fake.local", - limits: new ModelLimits({}), + route: OpenAIChat.route, }) +const decodeLLMRequest = Schema.decodeUnknownSync(LLMRequest as unknown as Schema.Decoder) +const decodeLLMEvent = Schema.decodeUnknownSync(LLMEvent as unknown as Schema.Decoder) + describe("llm schema", () => { test("decodes a minimal request", () => { const input: unknown = { @@ -22,26 +25,26 @@ describe("llm schema", () => { generation: {}, } - const decoded = Schema.decodeUnknownSync(LLMRequest)(input) + const decoded = decodeLLMRequest(input) expect(decoded.id).toBe("req_1") expect(decoded.messages[0]?.content[0]?.type).toBe("text") }) test("accepts custom route ids", () => { - const decoded = Schema.decodeUnknownSync(LLMRequest)({ - model: { ...model, route: "custom-route" }, + const decoded = decodeLLMRequest({ + model: Model.update(model, { route: OpenAIResponses.route }), system: [], messages: [], tools: [], generation: {}, }) - expect(decoded.model.route).toBe("custom-route") + expect(decoded.model.route.id).toBe("openai-responses") }) test("rejects invalid event type", () => { - expect(() => Schema.decodeUnknownSync(LLMEvent)({ type: "bogus" })).toThrow() + expect(() => decodeLLMEvent({ type: "bogus" })).toThrow() }) test("finish constructors accept usage input", () => { diff --git a/packages/llm/test/tool-runtime.test.ts b/packages/llm/test/tool-runtime.test.ts index 81389a466b2..6c85e2d38c2 100644 --- a/packages/llm/test/tool-runtime.test.ts +++ b/packages/llm/test/tool-runtime.test.ts @@ -1,9 +1,10 @@ import { describe, expect } from "bun:test" import { Effect, Schema, Stream } from "effect" import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice } from "../src" -import { LLMClient } from "../src/route" +import { Auth, LLMClient } from "../src/route" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as OpenAIChat from "../src/protocols/openai-chat" +import * as OpenAIResponses from "../src/protocols/openai-responses" import { tool, ToolFailure, type ToolExecuteContext } from "../src/tool" import { ToolRuntime } from "../src/tool-runtime" import { it } from "./lib/effect" @@ -12,11 +13,9 @@ import { dynamicResponse, scriptedResponses } from "./lib/http" import { deltaChunk, finishChunk, toolCallChunk } from "./lib/openai-chunks" import { sseEvents } from "./lib/sse" -const model = OpenAIChat.model({ - id: "gpt-4o-mini", - baseURL: "https://api.openai.test/v1/", - headers: { authorization: "Bearer test" }, -}) +const model = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-4o-mini" }) const Json = Schema.fromJsonString(Schema.Unknown) const decodeJson = Schema.decodeUnknownSync(Json) @@ -141,6 +140,45 @@ describe("LLMClient tools", () => { }), ) + it.effect("preserves content tool results from dynamic tools", () => + Effect.gen(function* () { + const screenshot = tool({ + description: "Capture a screenshot.", + jsonSchema: { type: "object", properties: {} }, + execute: () => + Effect.succeed({ + type: "content" as const, + value: [ + { type: "text" as const, text: "Screenshot captured." }, + { type: "media" as const, mediaType: "image/png", data: "AAAA" }, + ], + }), + }) + + const events = Array.from( + yield* LLMClient.stream({ request: baseRequest, tools: { screenshot } }).pipe( + Stream.runCollect, + Effect.provide( + scriptedResponses([sseEvents(toolCallChunk("call_1", "screenshot", "{}"), finishChunk("tool_calls"))]), + ), + ), + ) + + expect(events.find(LLMEvent.is.toolResult)).toMatchObject({ + type: "tool-result", + id: "call_1", + name: "screenshot", + result: { + type: "content", + value: [ + { type: "text", text: "Screenshot captured." }, + { type: "media", mediaType: "image/png", data: "AAAA" }, + ], + }, + }) + }), + ) + it.effect("executes tool calls for one step without looping by default", () => Effect.gen(function* () { const layer = scriptedResponses([ @@ -249,7 +287,9 @@ describe("LLMClient tools", () => { yield* TestToolRuntime.runTools({ request: LLM.updateRequest(baseRequest, { - model: AnthropicMessages.model({ id: "claude-sonnet-4-5", apiKey: "test" }), + model: AnthropicMessages.route + .with({ auth: Auth.header("x-api-key", "test") }) + .model({ id: "claude-sonnet-4-5" }), }), tools: { get_weather }, }).pipe(Stream.runCollect, Effect.provide(layer)) @@ -270,6 +310,80 @@ describe("LLMClient tools", () => { }), ) + it.effect("replays encrypted OpenAI reasoning items with tool outputs", () => + Effect.gen(function* () { + const bodies: unknown[] = [] + const layer = dynamicResponse((input) => + Effect.sync(() => { + bodies.push(decodeJson(input.text)) + return input.respond( + bodies.length === 1 + ? sseEvents( + { + type: "response.output_item.added", + item: { type: "reasoning", id: "rs_1", encrypted_content: null }, + }, + { type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 }, + { type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 }, + { + type: "response.output_item.done", + item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" }, + }, + { + type: "response.output_item.added", + item: { + type: "function_call", + id: "item_1", + call_id: "call_1", + name: "get_weather", + arguments: "", + }, + }, + { type: "response.function_call_arguments.delta", item_id: "item_1", delta: '{"city":"Paris"}' }, + { + type: "response.output_item.done", + item: { + type: "function_call", + id: "item_1", + call_id: "call_1", + name: "get_weather", + arguments: '{"city":"Paris"}', + }, + }, + { type: "response.completed", response: {} }, + ) + : sseEvents( + { type: "response.output_text.delta", item_id: "msg_1", delta: "Done." }, + { type: "response.completed", response: {} }, + ), + { headers: { "content-type": "text/event-stream" } }, + ) + }), + ) + + yield* TestToolRuntime.runTools({ + request: LLM.request({ + model: OpenAIResponses.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-5.5" }), + prompt: "Use the tool.", + providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } }, + }), + tools: { get_weather }, + }).pipe(Stream.runCollect, Effect.provide(layer)) + + expect(bodies[1]).toMatchObject({ + include: ["reasoning.encrypted_content"], + input: [ + { role: "user" }, + { type: "reasoning", id: "rs_1", summary: [], encrypted_content: "encrypted-state" }, + { type: "function_call", call_id: "call_1", name: "get_weather" }, + { type: "function_call_output", call_id: "call_1" }, + ], + }) + }), + ) + it.effect("emits tool-error for unknown tools so the model can self-correct", () => Effect.gen(function* () { const layer = scriptedResponses([ @@ -496,7 +610,9 @@ describe("LLMClient tools", () => { const events = Array.from( yield* TestToolRuntime.runTools({ request: LLM.updateRequest(baseRequest, { - model: AnthropicMessages.model({ id: "claude-sonnet-4-5", apiKey: "test" }), + model: AnthropicMessages.route + .with({ auth: Auth.header("x-api-key", "test") }) + .model({ id: "claude-sonnet-4-5" }), }), tools: {}, }).pipe(Stream.runCollect, Effect.provide(layer)), diff --git a/packages/llm/test/tool.types.ts b/packages/llm/test/tool.types.ts index 4ffc30c986c..1fce9fd231f 100644 --- a/packages/llm/test/tool.types.ts +++ b/packages/llm/test/tool.types.ts @@ -1,10 +1,11 @@ import { Effect, Schema } from "effect" import { LLM } from "../src" import * as OpenAIChat from "../src/protocols/openai-chat" +import { Auth } from "../src/route" import { tool } from "../src/tool" const request = LLM.request({ - model: OpenAIChat.model({ id: "gpt-4o-mini", apiKey: "fixture" }), + model: OpenAIChat.route.with({ auth: Auth.bearer("fixture") }).model({ id: "gpt-4o-mini" }), prompt: "Use the tool.", }) diff --git a/packages/opencode/.gitignore b/packages/opencode/.gitignore index 81668738db7..932b09924cb 100644 --- a/packages/opencode/.gitignore +++ b/packages/opencode/.gitignore @@ -3,9 +3,6 @@ dist dist-* gen app.log -src/provider/models-snapshot.js -src/provider/models-snapshot.d.ts -src/provider/models-snapshot.json script/build-*.ts temporary-*.md .artifacts diff --git a/packages/opencode/CHANGELOG.md b/packages/opencode/CHANGELOG.md index d1caadd0bae..2be430825c4 100644 --- a/packages/opencode/CHANGELOG.md +++ b/packages/opencode/CHANGELOG.md @@ -1,5 +1,278 @@ # @kilocode/cli +## 7.3.54 + +### Patch Changes + +- [#11555](https://github.com/Kilo-Org/kilocode/pull/11555) [`5c1dcdf`](https://github.com/Kilo-Org/kilocode/commit/5c1dcdffca2fba153efe62a974727a066de25ba9) - Use the correct High and Max thinking variants for GLM 5.2 on OpenCode Go and compatible providers. + +## 7.3.53 + +### Minor Changes + +- [#11468](https://github.com/Kilo-Org/kilocode/pull/11468) [`27bd206`](https://github.com/Kilo-Org/kilocode/commit/27bd20680ce4be32ab69126169d0c56c77bf3b02) - Search titles and high-signal transcript content across all local sessions with the recall tool. + +### Patch Changes + +- [#11533](https://github.com/Kilo-Org/kilocode/pull/11533) [`15f42d4`](https://github.com/Kilo-Org/kilocode/commit/15f42d4bec51bbb127636738275f36fdc07e7b33) - Restore bounded text-file reads and keep zero-limit pagination and Unicode truncation from producing unusable tool output. + +- Updated dependencies [[`6c55c28`](https://github.com/Kilo-Org/kilocode/commit/6c55c28ec345a6d90d2d7a4e345abf962f208e29)]: + - @kilocode/kilo-gateway@7.3.53 + - @kilocode/kilo-indexing@7.3.53 + - @kilocode/kilo-telemetry@7.3.53 + - @opencode-ai/ui@7.3.53 + +## 7.3.52 + +### Patch Changes + +- [#11450](https://github.com/Kilo-Org/kilocode/pull/11450) [`cc924a6`](https://github.com/Kilo-Org/kilocode/commit/cc924a67d9b190ccffebaefa983213e173db54d8) - Changes from opencode v1.15.9 to v1.15.13 upstream: + - Core Improvements: Added `headerTimeout` config for provider requests, with a 10s default for default OpenAI setups. + - Core Improvements: Experimental background agents now push updates without polling. + - Core Improvements: You can now set only `modalities.input` or `modalities.output` in config. (@robposch) + - Core Improvements: Remote-backed projects now resolve a stable project identity. + - Core Improvements: ACP integrations can now send prompts, slash commands, and usage updates through `acp-next` + - Core Improvements: Added WebSocket transport for OpenAI responses on supported channels (set KILO_EXPERIMENTAL_WEBSOCKETS=true) + - Core Improvements: Sessions can now store custom metadata through the API and SDK. (@shantur) + - Core Improvements: Config now loads from the opened location upward, so directory-specific settings and provider policies apply more predictably. + - Core Bugfixes: Dynamically added MCP servers now disconnect cleanly when removed. + - Core Bugfixes: DigitalOcean inference now uses your OAuth token directly instead of creating a MAK. (@Spherrrical) + - Core Bugfixes: Config loading now falls back cleanly when user info is unavailable. + - Core Bugfixes: Fixed Google tool calling after the upstream tool ID regression. + - Core Bugfixes: Experimental flags can now override the umbrella experimental flag. + - Core Bugfixes: Resumed sessions no longer continue orphaned interrupted tools. (@edevil) + - Core Bugfixes: OpenAI reasoning summaries now render as separate blocks. + - Core Bugfixes: Updated Google Vertex support for reasoning signatures. + - Core Bugfixes: The shell tool now advertises your configured timeout to the model. + - Core Bugfixes: Enabled adaptive reasoning controls for Anthropic Opus 4.7+ models + - Core Bugfixes: Allowed colons in passwords (@neriousy) + - Core Bugfixes: Sped up warm `acp-next` model and config switches + - Core Bugfixes: Improved first-session `acp-next` startup time + - Core Bugfixes: Kept OpenAI WebSocket response timeouts active + - Core Bugfixes: Retried failed OpenAI WebSocket streams before falling back + - Core Bugfixes: Handled `acp-next` permission prompts correctly + - Core Bugfixes: Used the persisted session directory for existing-session requests + - Core Bugfixes: Forwarded remote workspace request bodies correctly + - Core Bugfixes: Supported custom base URLs for OpenAI WebSocket responses (@Tarquinen) + - Core Bugfixes: Gateway Anthropic Opus 4.7+ adaptive reasoning now keeps summarized thinking instead of returning empty thinking blocks. + - TUI Improvements: Made the prompt resize with terminal width and added prompt size config. (@bjschafer) + - TUI Improvements: Added a workspace management dialog + - TUI Bugfixes: Accelerated diff viewer scrolling. + - TUI Bugfixes: External editors now open from the worktree directory when available. + - TUI Bugfixes: Kept session navigation working while prompt modes are open + - TUI Bugfixes: Restored the thinking spinner + - TUI Bugfixes: Surfaced subagent retry status + - TUI Bugfixes: Fixed opening editors from non-Git project paths (@OpeOginni) + - TUI Bugfixes: Wrapped inline tool rows now stay aligned, and failed inline tools can expand their error details in place. + - Extensions Improvements: Added a `dispose` hook for plugins. + - Extensions Bugfixes: Fixed Codex plugin requests to send the expected session ID header. + +## 7.3.51 + +### Minor Changes + +- [#11478](https://github.com/Kilo-Org/kilocode/pull/11478) [`9611c8b`](https://github.com/Kilo-Org/kilocode/commit/9611c8b1ef2d623f7c486c5a0019ee0f590ce02d) - Support stopping the daemon with `kilo console stop` and keeping console or daemon commands attached with `--foreground` + +- [#10005](https://github.com/Kilo-Org/kilocode/pull/10005) [`1d030dc`](https://github.com/Kilo-Org/kilocode/commit/1d030dcbbb6782181af684c8321b7349682bba5f) - Support `kilo run --command compact` and `--command summarize` to compact the current session, matching the TUI's `/compact` and `/summarize` slash commands. + +## 7.3.50 + +### Minor Changes + +- [#11421](https://github.com/Kilo-Org/kilocode/pull/11421) [`ccec216`](https://github.com/Kilo-Org/kilocode/commit/ccec2162383a6f378ed5e62d630720607d185209) - Show a BYOK badge for Kilo Gateway models that can use an enabled personal or organization provider key. + +- [#11028](https://github.com/Kilo-Org/kilocode/pull/11028) [`a6ded9b`](https://github.com/Kilo-Org/kilocode/commit/a6ded9b60a65f41a9a68f65d8ababa478cf51f52) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Display local and network URLs when the server binds to 0.0.0.0 + +### Patch Changes + +- [#11412](https://github.com/Kilo-Org/kilocode/pull/11412) [`2c9e72c`](https://github.com/Kilo-Org/kilocode/commit/2c9e72c14a87387199fd42546746bbea30aa1570) - Deny provider data collection for Kilo Gateway requests when prompt-training models are hidden. + +- [#11301](https://github.com/Kilo-Org/kilocode/pull/11301) [`081b653`](https://github.com/Kilo-Org/kilocode/commit/081b65325f539a4c71db90ce9a89dba4cfa3226f) - Add a privacy filter to the Console model explorer that hides Kilo Gateway models whose providers may use prompts for training. + +- [#11026](https://github.com/Kilo-Org/kilocode/pull/11026) [`e2ebf8b`](https://github.com/Kilo-Org/kilocode/commit/e2ebf8b7c8299cb42e68ef33e74507caef448206) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Skip automatic browser launch on Linux when no display is detected. + +- [#11212](https://github.com/Kilo-Org/kilocode/pull/11212) [`8649ab6`](https://github.com/Kilo-Org/kilocode/commit/8649ab6dcd04e219b0d4bf98787fc4c2e9353c95) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Show docs URL in dialog when no display server is detected on headless Linux systems + +- [#11319](https://github.com/Kilo-Org/kilocode/pull/11319) [`fb37d9c`](https://github.com/Kilo-Org/kilocode/commit/fb37d9c773791f3ec86379dcef9221797ce50f5c) Thanks [@grandmaster451](https://github.com/grandmaster451)! - Show the docs URL in an alert dialog when the browser cannot be opened on headless systems instead of silently failing. + +- [#11455](https://github.com/Kilo-Org/kilocode/pull/11455) [`4d09333`](https://github.com/Kilo-Org/kilocode/commit/4d0933371ca9be212cdd0357605e250ebacf7e1b) - Hide reverted provider errors so Redo controls remain visible after rewinding a session. + +- [#11475](https://github.com/Kilo-Org/kilocode/pull/11475) [`3d4ccc2`](https://github.com/Kilo-Org/kilocode/commit/3d4ccc25cf1caee91af93f50be127190bead2a23) - Preserve custom subagent tool permissions when tasks inherit restrictions from their parent agent. + +- [#11453](https://github.com/Kilo-Org/kilocode/pull/11453) [`f7e68d1`](https://github.com/Kilo-Org/kilocode/commit/f7e68d19d9d8b23b087d3c7c92d487abced8d7ec) - Limit completion sounds to parent agent sessions. + +- Updated dependencies [[`ccec216`](https://github.com/Kilo-Org/kilocode/commit/ccec2162383a6f378ed5e62d630720607d185209), [`2c9e72c`](https://github.com/Kilo-Org/kilocode/commit/2c9e72c14a87387199fd42546746bbea30aa1570), [`f7e68d1`](https://github.com/Kilo-Org/kilocode/commit/f7e68d19d9d8b23b087d3c7c92d487abced8d7ec)]: + - @kilocode/kilo-gateway@7.4.0 + - @kilocode/sdk@7.3.50 + - @kilocode/kilo-indexing@7.3.50 + - @kilocode/kilo-telemetry@7.3.50 + - @kilocode/plugin@7.3.50 + - @opencode-ai/ui@7.3.50 + - @kilocode/plugin-atomic-chat@7.3.50 + +## 7.3.49 + +## 7.3.48 + +### Minor Changes + +- [#11182](https://github.com/Kilo-Org/kilocode/pull/11182) [`973d02c`](https://github.com/Kilo-Org/kilocode/commit/973d02cfd15b3bf3eefefe92e7fb61059eba26f7) - Share the main codebase index with Agent Manager worktrees while indexing and searching only each worktree's changed files. + +- [#10781](https://github.com/Kilo-Org/kilocode/pull/10781) [`66af690`](https://github.com/Kilo-Org/kilocode/commit/66af6907005b99bb39a0869b35dfe1ec180cc0b5) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Add opt-in Unicode or emoji terminal title indicators for sessions that are working, need attention, or have finished. + +### Patch Changes + +- [#11242](https://github.com/Kilo-Org/kilocode/pull/11242) [`9211000`](https://github.com/Kilo-Org/kilocode/commit/9211000aadd909f0d46746604c3e963966a59660) - Support unauthenticated OpenAI-compatible endpoints for codebase indexing without requiring a placeholder API key. + +- [#11305](https://github.com/Kilo-Org/kilocode/pull/11305) [`04ed322`](https://github.com/Kilo-Org/kilocode/commit/04ed322aad65c43e7817535389ab6a45c247db75) - Prevent snapshot initialization progress from blocking conversations after the slow repository prompt. + +- [#11249](https://github.com/Kilo-Org/kilocode/pull/11249) [`2c30dc7`](https://github.com/Kilo-Org/kilocode/commit/2c30dc75ce18c018f603a30d1c9e3c70fe8fc036) - Show a clear, retryable provider rate-limit error instead of raw response JSON in chat. + +- [#11171](https://github.com/Kilo-Org/kilocode/pull/11171) [`04ebc74`](https://github.com/Kilo-Org/kilocode/commit/04ebc7413ce4e5e55ebc098c85c7cec449363ad9) - Hide TUI news after they have been opened and add a button to close the news dialog. + +- [#10929](https://github.com/Kilo-Org/kilocode/pull/10929) [`9329682`](https://github.com/Kilo-Org/kilocode/commit/9329682775b19fb1ac0e4f08d3c1b3904b6815ea) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Make `/copy` copy the latest agent response and use `/copy-session` for session transcripts. + +- [#10091](https://github.com/Kilo-Org/kilocode/pull/10091) [`be234fa`](https://github.com/Kilo-Org/kilocode/commit/be234fa92613cc47a69c116e6f297559f8c736eb) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Always deny tool calls for title, summarize, and compaction + +- [#11264](https://github.com/Kilo-Org/kilocode/pull/11264) [`f78e54c`](https://github.com/Kilo-Org/kilocode/commit/f78e54c81c67a1b79af8b98ec4af3686aa716bfd) - Fix upgrades to resolve Kilo CLI packages and releases instead of OpenCode packages and versions. + +- [#11347](https://github.com/Kilo-Org/kilocode/pull/11347) [`b518a76`](https://github.com/Kilo-Org/kilocode/commit/b518a76aea020b3320666aa0a69a113516d0a1e0) - Identify Kilo in provider request user-agent headers instead of OpenCode. + +- [#11279](https://github.com/Kilo-Org/kilocode/pull/11279) [`e91eef2`](https://github.com/Kilo-Org/kilocode/commit/e91eef2b384e64ffdbbd5d9fad99d534ecb7a2e8) - Show current-worktree sessions by default in the TUI sessions dialog and keep all/current scope toggling working when a scope has no sessions. + +- [#11158](https://github.com/Kilo-Org/kilocode/pull/11158) [`8ff8371`](https://github.com/Kilo-Org/kilocode/commit/8ff83711766ff6b18ea23d1990d6fedd8e79c5ae) - Add a shared model setting to hide Kilo Gateway models that may train on your prompts across Kilo clients. + +- [#11270](https://github.com/Kilo-Org/kilocode/pull/11270) [`c5d39d0`](https://github.com/Kilo-Org/kilocode/commit/c5d39d090c34f9fea834718a799bb921ee69df3c) - Replace remaining OpenCode-branded CLI and TUI copy with Kilo branding. + +- [#11279](https://github.com/Kilo-Org/kilocode/pull/11279) [`2f69c13`](https://github.com/Kilo-Org/kilocode/commit/2f69c132b0d968e08a139681305471fc3ca627ed) - Show Agent Manager and other Git worktrees in the Kilo Console project view. + +- [#11291](https://github.com/Kilo-Org/kilocode/pull/11291) [`4436139`](https://github.com/Kilo-Org/kilocode/commit/4436139fab57ccb65c33ac3d303f38a9efd4733b) - Load the bundled Atomic Chat integration without attempting to install an unpublished npm plugin. + +- [#11236](https://github.com/Kilo-Org/kilocode/pull/11236) [`1511d13`](https://github.com/Kilo-Org/kilocode/commit/1511d13b3f7f20001d2111f14bdfae7155372cf8) Thanks [@kapelame](https://github.com/kapelame)! - Add an instant/thinking reasoning toggle for MiniMax M-series models, matching the existing glm/kimi/qwen behavior. + +- [#11170](https://github.com/Kilo-Org/kilocode/pull/11170) [`3845918`](https://github.com/Kilo-Org/kilocode/commit/38459184f27a5a22d9314fcb6e113ddec7b2f0e2) Thanks [@johnnyeric](https://github.com/johnnyeric)! - Make native Plan mode follow Architect-style planning behavior while preserving Plan mode restrictions and repo-root plan files. + +- [#11257](https://github.com/Kilo-Org/kilocode/pull/11257) [`f42789d`](https://github.com/Kilo-Org/kilocode/commit/f42789d0ef5585aad4080bdc5c96856675cd9503) - Changes from opencode v1.14.51 to v1.15.4 upstream: + - Core Improvements: Clarified how to recover when the npm package is installed without its native binary. + - Core Improvements: Reduced unnecessary prompting around shell, task, and todo flows. + - Core Bugfixes: Ignored invalid exports in custom tool modules instead of failing tool loading. + - Core Bugfixes: Ignored project instruction lookup errors so sessions keep loading when project instruction discovery fails. + - Core Bugfixes: Fixed versioned event projector lookups so event replay uses the right handlers. + - Core Bugfixes: Avoid duplicate consecutive entries in prompt history. + - Core Bugfixes: Show full config validation errors during TUI startup instead of a generic failure. + - Core Bugfixes: Fixed npm installs so the CLI can recover and fetch the right native binary on more setups. + - Core Bugfixes: Fixed multiline `@` mentions in prompts. + - Core Bugfixes: Preserved custom tool metadata from Zod schemas. + - Core Bugfixes: Preserved custom tool argument descriptions in generated schemas. + - Core Bugfixes: Fixed file watching in repos where `.git` is a symlink. (@kagura-agent) + - Core Bugfixes: Fixed sync events not reaching project-scoped subscribers in injected instances. + - Core Bugfixes: Reduced wasted work when reading very large files after output truncation. + - Core Bugfixes: Fixed project-scoped bus events so file watcher and update notifications reach the right instance. + - Core Bugfixes: Fixed custom LSP servers not sending refresh events after they initialize. + - Core Bugfixes: Hid background subagent task instructions unless experimental background mode is enabled. + - TUI Improvements: Added a collapsed thinking view that can be expanded inline. + - TUI Improvements: Added pinned sessions with quick-switch slots in the session picker. + - TUI Improvements: Newly pinned sessions now stay at the end of the pinned list instead of jumping to the top. + - TUI Improvements: Made Markdown H1 headings easier to distinguish. + - TUI Bugfixes: Fixed thinking mode defaults so reasoning starts collapsed consistently. + - TUI Bugfixes: Limited session quick-switching to pinned sessions. + - TUI Bugfixes: Fixed Markdown table rendering in chat output. + - TUI Bugfixes: Fixed `kilo run --agent` resolving project-local agents. + - TUI Bugfixes: Fixed async commands losing the active instance context, which could break agent generation and GitHub-driven runs. + +- [#11356](https://github.com/Kilo-Org/kilocode/pull/11356) [`326ff35`](https://github.com/Kilo-Org/kilocode/commit/326ff351460342f93b0bf97f0beb6383357c5d05) - Changes from opencode v1.15.4 to v1.15.9 upstream: + - Core Improvements: Preview the native OpenAI runtime path behind an experimental flag + - Core Improvements: Add `--replay` and `--replay-limit` to show recent history when resuming interactive runs + - Core Improvements: Added a diff viewer in the TUI for reviewing changes. + - Core Improvements: Collapsed single-child directories in the diff viewer file tree. + - Core Improvements: Added shell mode to the `run` prompt. + - Core Improvements: Replaced subagent tabs with an on-demand picker in `run`. + - Core Improvements: Plugin file load errors no longer break the rest of plugin loading. + - Core Improvements: Anthropic API-key models now use the native runtime. + - Core Improvements: The v2 HTTP API now exposes structured public error schemas. + - Core Improvements: Added Grok OAuth sign-in, including device-code login. (@Jaaneek) + - Core Improvements: Redesigned the diff viewer with a file tree and refreshed layout. + - Core Bugfixes: Fix plugin tools using `ask` so tool calls complete correctly + - Core Bugfixes: Reduce missed `/event` updates caused by a subscription race + - Core Bugfixes: Sort the v2 session list by most recently updated + - Core Bugfixes: Zed editor context now only activates inside Zed terminals. + - Core Bugfixes: Agent and command names now resolve correctly from relative config paths. + - Core Bugfixes: Invalid `KILO_PERMISSION` JSON no longer crashes startup. + - Core Bugfixes: Plugin tools with missing `args` no longer break tool loading. + - Core Bugfixes: Restored legacy `PgUp` and `PgDn` TUI keybind aliases. + - Core Bugfixes: Native runtime now prefers the console provider token for OpenCode models. + - Core Bugfixes: V2 session APIs now return safe `UnknownError` responses with log reference IDs when stored messages are corrupt. + - Core Bugfixes: Generic API 500s no longer expose config details from server errors. + - Core Bugfixes: Unknown API errors now include reference IDs so you can match responses to server logs. + - Core Bugfixes: V2 session APIs now return `503 ServiceUnavailableError` for mutations that are not available yet. + - Core Bugfixes: V2 session APIs now return `SessionNotFoundError` for missing sessions. + - Core Bugfixes: Deduped concurrent Codex OAuth refreshes to avoid repeated refresh failures. (@cooper-oai) + - Core Bugfixes: Restored native OpenAI OAuth requests. + - Core Bugfixes: Tool schema failures now surface as friendly tool errors. + - Core Bugfixes: Added PDF attachment support for Grok. + - Core Bugfixes: Restored OpenAI reasoning streams. + - Core Bugfixes: Return to the previous screen when closing the diff viewer. + - Core Bugfixes: Show clearer errors when a default model is invalid or unavailable. + - Core Bugfixes: Surface missing PTY session errors instead of failing generically. + - Core Bugfixes: Improve diff viewer empty states and context handling. + - Core Bugfixes: Show clearer errors when a skill invocation fails as expected. + - Core Bugfixes: Show clearer errors when an installation upgrade fails. + - Core Bugfixes: Show clearer project not found errors from the HTTP API. + - Core Bugfixes: Return PTY error bodies from the HTTP API. + - Core Bugfixes: Enable the diff viewer by default. + - Core Bugfixes: Return MCP server not found errors from the HTTP API. + - Core Bugfixes: Let MCP OAuth configs set a callback port and include configured scopes in client metadata. (@sebin) + - Core Bugfixes: Use working Vertex Anthropic endpoints for `us` and `eu` multi-region setups. (@JPFrancoia) + - Core Bugfixes: Return session busy error bodies from the HTTP API. + - Core Bugfixes: Preserve native reasoning continuation metadata across turns. + - TUI Improvements: Refresh the prompt layout after pasting content + - TUI Improvements: The diff viewer now focuses the first file automatically. + - TUI Improvements: Copy the current worktree path from the command palette. + - TUI Bugfixes: Keep file references scoped to the current workspace + - TUI Bugfixes: Preserve pasted prompt content when copying + - TUI Bugfixes: Collapse very long tool output lines to keep the layout readable + - TUI Bugfixes: Use a higher-contrast paste summary badge color in some themes (@kagura-agent) + - TUI Bugfixes: Imported sessions now refresh their directory and relative path fields correctly. (@OpeOginni) + - TUI Bugfixes: Collapsed thinking labels now use clearer punctuation. + - TUI Bugfixes: New sessions now default to the local project. + - TUI Bugfixes: Single-select question checkmarks no longer run into option labels. + - TUI Bugfixes: Refine diff viewer keyboard shortcuts. + - TUI Bugfixes: Restore question prompt key handling. + - TUI Bugfixes: Keep the spinner color aligned with the active agent. (@OpeOginni) + +- [#11245](https://github.com/Kilo-Org/kilocode/pull/11245) [`046b03a`](https://github.com/Kilo-Org/kilocode/commit/046b03a19de2b4017211efb70d0641499789efa8) Thanks [@johnnyeric](https://github.com/johnnyeric)! - Restore session timestamp prefixes for generated plan filenames while preserving descriptive model-chosen names. + +- [#9807](https://github.com/Kilo-Org/kilocode/pull/9807) [`9394100`](https://github.com/Kilo-Org/kilocode/commit/93941001f6211622318dab1a7e6ec6c420dbd612) Thanks [@truffle-dev](https://github.com/truffle-dev)! - Prevent unreachable telemetry endpoints from blocking or failing completed CLI commands. + +- [#11279](https://github.com/Kilo-Org/kilocode/pull/11279) [`8c1cdf5`](https://github.com/Kilo-Org/kilocode/commit/8c1cdf53a94a00f914a3c7f392b2569d422985ad) - Keep expanded Kilo Console file diffs open while resizing the context sidebar. + +- [#11279](https://github.com/Kilo-Org/kilocode/pull/11279) [`2f69c13`](https://github.com/Kilo-Org/kilocode/commit/2f69c132b0d968e08a139681305471fc3ca627ed) - Keep Kilo Console terminal sessions open when changing diff layout and other console preferences. + +- [#11354](https://github.com/Kilo-Org/kilocode/pull/11354) [`b2eef5c`](https://github.com/Kilo-Org/kilocode/commit/b2eef5cff413d8e61798e9187c9740fd0ac7273f) - Prevent the bundled Atomic Chat plugin from triggering an npm installation. + +- [#11373](https://github.com/Kilo-Org/kilocode/pull/11373) [`f21a34a`](https://github.com/Kilo-Org/kilocode/commit/f21a34a1e63107da085eb9e57172ca6025d2dbe0) - Skip attention sounds when a session is manually interrupted. + +- [#11295](https://github.com/Kilo-Org/kilocode/pull/11295) [`2fa0890`](https://github.com/Kilo-Org/kilocode/commit/2fa0890928f7dd060125ad4f4083b8bd2bf3e69b) - Restore speech input when profile details are unavailable, move transcription model selection to the Models tab, and default transcription to Whisper Large V3 Turbo. + +- [#9758](https://github.com/Kilo-Org/kilocode/pull/9758) [`8db7b68`](https://github.com/Kilo-Org/kilocode/commit/8db7b685837e015dc922825f03641a221e5becf7) - Restore files to their original paths when reverting a task that moved or renamed them. + +- [#11410](https://github.com/Kilo-Org/kilocode/pull/11410) [`344a6a5`](https://github.com/Kilo-Org/kilocode/commit/344a6a5f0f8377d8ab38792e6141d08947a7dc19) - Keep server controls and events connected to active sessions and subagents. + +- [#11221](https://github.com/Kilo-Org/kilocode/pull/11221) [`987da27`](https://github.com/Kilo-Org/kilocode/commit/987da2728731e1da1c974996b5bcddafe745cea7) - Show shared provider descriptions and provider icons in JetBrains and VS Code provider settings. + +- [#11262](https://github.com/Kilo-Org/kilocode/pull/11262) [`0903183`](https://github.com/Kilo-Org/kilocode/commit/090318379956d5fd200fa3182b525f746ed6a442) - Expose the prompt-training model filter in the Kilo Console model settings. + +- [#10758](https://github.com/Kilo-Org/kilocode/pull/10758) [`e511b23`](https://github.com/Kilo-Org/kilocode/commit/e511b230ab87c3b1a594a7e1ac12e44a096a813f) Thanks [@cooper-oai](https://github.com/cooper-oai)! - Prevent concurrent Kilo processes from reusing a ChatGPT Codex refresh token. + +- Updated dependencies [[`9211000`](https://github.com/Kilo-Org/kilocode/commit/9211000aadd909f0d46746604c3e963966a59660), [`2fa0890`](https://github.com/Kilo-Org/kilocode/commit/2fa0890928f7dd060125ad4f4083b8bd2bf3e69b), [`973d02c`](https://github.com/Kilo-Org/kilocode/commit/973d02cfd15b3bf3eefefe92e7fb61059eba26f7), [`66af690`](https://github.com/Kilo-Org/kilocode/commit/66af6907005b99bb39a0869b35dfe1ec180cc0b5)]: + - @kilocode/kilo-indexing@7.4.0 + - @kilocode/sdk@7.4.0 + - @kilocode/plugin@7.3.47 + - @opencode-ai/ui@7.3.47 + - @kilocode/kilo-gateway@7.3.47 + - @kilocode/plugin-atomic-chat@7.3.47 + - @kilocode/kilo-telemetry@7.3.47 + ## 7.3.46 ### Patch Changes diff --git a/packages/opencode/Dockerfile b/packages/opencode/Dockerfile index 5f197528dd4..73ce096ea08 100644 --- a/packages/opencode/Dockerfile +++ b/packages/opencode/Dockerfile @@ -7,10 +7,16 @@ ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH} RUN apk add libgcc libstdc++ ripgrep FROM base AS build-amd64 -COPY dist/@kilocode/cli-linux-x64-baseline-musl/bin/kilo /usr/local/bin/kilo +# kilocode_change start +COPY dist/@kilocode/cli-linux-x64-baseline-musl/bin/kilo dist/@kilocode/cli-linux-x64-baseline-musl/bin/bwrap /usr/local/bin/ +COPY dist/@kilocode/cli-linux-x64-baseline-musl/bin/licenses /usr/local/share/licenses/kilo +# kilocode_change end FROM base AS build-arm64 -COPY dist/@kilocode/cli-linux-arm64-musl/bin/kilo /usr/local/bin/kilo +# kilocode_change start +COPY dist/@kilocode/cli-linux-arm64-musl/bin/kilo dist/@kilocode/cli-linux-arm64-musl/bin/bwrap /usr/local/bin/ +COPY dist/@kilocode/cli-linux-arm64-musl/bin/licenses /usr/local/share/licenses/kilo +# kilocode_change end ARG TARGETARCH FROM build-${TARGETARCH} diff --git a/packages/opencode/migration/20260511173437_session-metadata/migration.sql b/packages/opencode/migration/20260511173437_session-metadata/migration.sql new file mode 100644 index 00000000000..0ce73631f0d --- /dev/null +++ b/packages/opencode/migration/20260511173437_session-metadata/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `session` ADD `metadata` text; \ No newline at end of file diff --git a/packages/opencode/migration/20260511173437_session-metadata/snapshot.json b/packages/opencode/migration/20260511173437_session-metadata/snapshot.json new file mode 100644 index 00000000000..07d9ec016a8 --- /dev/null +++ b/packages/opencode/migration/20260511173437_session-metadata/snapshot.json @@ -0,0 +1,1500 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "85d2086c-3c95-4706-90b0-f7480b73db5c", + "prevIds": ["fdfcccee-fb3a-481f-b801-b9835fa30d5d"], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "columns": ["active_account_id"], + "tableTo": "account", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": ["message_id"], + "tableTo": "message", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": ["project_id"], + "tableTo": "project", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": ["session_id"], + "tableTo": "session", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": ["aggregate_id"], + "tableTo": "event_sequence", + "columnsTo": ["aggregate_id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": ["email", "url"], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": ["session_id", "position"], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": ["project_id"], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["session_id"], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": ["aggregate_id"], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 92de88c24c3..462917d3d61 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.46", + "version": "7.3.54", "name": "@kilocode/cli", "type": "module", "license": "MIT", @@ -10,6 +10,8 @@ "test": "bun run script/test-runner.ts", "test:ci": "bun run script/test-runner.ts --ci", "test:httpapi": "bun run script/httpapi-exercise.ts --mode coverage --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode auth --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode effect --fail-on-missing --fail-on-skip", + "bench:test": "bun run script/bench-test-suite.ts", + "profile:test": "bun run script/profile-test-files.ts", "build": "bun run script/build.ts", "fix-node-pty": "bun run script/fix-node-pty.ts", "dev": "bun run --conditions=browser ./src/index.ts", @@ -65,7 +67,8 @@ "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", - "@types/npmcli__arborist": "6.3.3" + "@types/npmcli__arborist": "6.3.3", + "@opencode-ai/http-recorder": "workspace:*" }, "dependencies": { "@actions/core": "1.11.1", @@ -102,6 +105,7 @@ "@kilocode/kilo-telemetry": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/plugin-atomic-chat": "workspace:*", + "@kilocode/sandbox": "workspace:*", "@kilocode/sdk": "workspace:*", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", @@ -109,6 +113,7 @@ "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", + "@opencode-ai/llm": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", @@ -124,6 +129,7 @@ "@silvia-odwyer/photon-node": "0.3.4", "@solid-primitives/event-bus": "1.1.2", "@solid-primitives/scheduled": "1.5.2", + "@types/ws": "8.18.1", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", "ai-gateway-provider": "3.1.2", @@ -138,7 +144,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.6.0", + "gitlab-ai-provider": "6.8.0", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "htmlparser2": "8.0.2", @@ -168,6 +174,7 @@ "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "which": "6.0.1", + "ws": "8.21.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "18.0.0", "zod": "catalog:" diff --git a/packages/opencode/parsers-config.ts b/packages/opencode/parsers-config.ts index 9bcb9e85c78..2f5e3e0bef4 100644 --- a/packages/opencode/parsers-config.ts +++ b/packages/opencode/parsers-config.ts @@ -296,5 +296,81 @@ export default { ], }, }, + { + filetype: "elixir", + wasm: "https://github.com/elixir-lang/tree-sitter-elixir/releases/download/v0.3.5/tree-sitter-elixir.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/elixir/highlights.scm", + ], + locals: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/elixir/locals.scm", + ], + }, + }, + { + filetype: "fsharp", + wasm: "https://github.com/ionide/tree-sitter-fsharp/releases/download/0.3.0/tree-sitter-fsharp.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/fsharp/highlights.scm", + ], + }, + }, + { + filetype: "r", + wasm: "https://github.com/r-lib/tree-sitter-r/releases/download/v1.2.0/tree-sitter-r.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/r/highlights.scm", + ], + locals: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/r/locals.scm", + ], + }, + }, + { + filetype: "make", + aliases: ["makefile"], + wasm: "https://github.com/tree-sitter-grammars/tree-sitter-make/releases/download/v1.1.1/tree-sitter-make.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/make/highlights.scm", + ], + }, + }, + { + filetype: "vim", + wasm: "https://github.com/tree-sitter-grammars/tree-sitter-vim/releases/download/v0.8.1/tree-sitter-vim.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/vim/highlights.scm", + ], + locals: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/vim/locals.scm", + ], + }, + }, + { + filetype: "xml", + wasm: "https://github.com/tree-sitter-grammars/tree-sitter-xml/releases/download/v0.7.0/tree-sitter-xml.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/xml/highlights.scm", + ], + locals: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/xml/locals.scm", + ], + }, + }, + { + filetype: "agda", + wasm: "https://github.com/tree-sitter/tree-sitter-agda/releases/download/v1.3.3/tree-sitter-agda.wasm", + queries: { + highlights: [ + "https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/agda/highlights.scm", + ], + }, + }, ], } diff --git a/packages/opencode/script/bench-test-suite.ts b/packages/opencode/script/bench-test-suite.ts new file mode 100644 index 00000000000..27ff7750c2f --- /dev/null +++ b/packages/opencode/script/bench-test-suite.ts @@ -0,0 +1,52 @@ +// Full-suite timing harness for the test-speed research in ../../perf/test-suite.md. +// Use this for periodic sanity checks; use profile-test-files.ts for discovery. +// Env: BENCH_WARMUPS=0 BENCH_RUNS=1 bun run bench:test +const warmups = Number(Bun.env.BENCH_WARMUPS ?? 0) +const runs = Number(Bun.env.BENCH_RUNS ?? 1) +const timings: number[] = [] + +if (!Number.isInteger(warmups) || warmups < 0) { + console.error("BENCH_WARMUPS must be a non-negative integer") + process.exit(1) +} +if (!Number.isInteger(runs) || runs < 1) { + console.error("BENCH_RUNS must be a positive integer") + process.exit(1) +} + +for (const index of Array.from({ length: warmups + runs }, (_, index) => index)) { + const measured = index >= warmups + const label = measured ? `run ${index - warmups + 1}/${runs}` : `warmup ${index + 1}/${warmups}` + const start = performance.now() + console.log(`bench:test ${label}`) + + const proc = Bun.spawn(["bun", "test", "--timeout", "30000"], { + cwd: import.meta.dir + "/..", + stdout: "inherit", + stderr: "inherit", + env: Bun.env, + }) + + const exitCode = await proc.exited + if (exitCode !== 0) { + console.error(`bench:test failed during ${label} with exit code ${exitCode}`) + process.exit(exitCode) + } + + const seconds = (performance.now() - start) / 1000 + console.log(`bench:test ${label} ${seconds.toFixed(3)}s`) + if (measured) timings.push(seconds) +} + +const sorted = timings.toSorted((a, b) => a - b) +const median = sorted[Math.floor(sorted.length / 2)] +const mean = timings.reduce((sum, timing) => sum + timing, 0) / timings.length +const best = sorted[0] ?? median +const worst = sorted.at(-1) ?? median + +console.log( + `bench:test median=${median.toFixed(3)}s mean=${mean.toFixed(3)}s best=${best.toFixed(3)}s worst=${worst.toFixed(3)}s`, +) +console.log(`METRIC test_suite_seconds=${median.toFixed(3)}`) +console.log(`METRIC test_suite_best_seconds=${best.toFixed(3)}`) +console.log(`METRIC test_suite_worst_seconds=${worst.toFixed(3)}`) diff --git a/packages/opencode/script/build-node.ts b/packages/opencode/script/build-node.ts old mode 100755 new mode 100644 index 515c6733dbf..53099f5b587 --- a/packages/opencode/script/build-node.ts +++ b/packages/opencode/script/build-node.ts @@ -11,7 +11,7 @@ const dir = path.resolve(__dirname, "..") process.chdir(dir) -await import("./generate.ts") +const generated = await import("./generate.ts") // Load migrations from migration directories const migrationDirs = ( @@ -45,13 +45,15 @@ console.log(`Loaded ${migrations.length} migrations`) await Bun.build({ target: "node", - entrypoints: ["./src/node.ts"], + entrypoints: ["./src/node.ts", "../kilo-sandbox/src/kilo-sandbox-mutation-worker.ts"], // kilocode_change outdir: "./dist/node", format: "esm", sourcemap: "linked", external: ["jsonc-parser", "@lydell/node-pty"], define: { KILO_MIGRATIONS: JSON.stringify(migrations), + KILO_MODELS_DEV: generated.modelsData, + KILO_SANDBOX_MUTATION_WORKER_PATH: `'./kilo-sandbox-mutation-worker.js'`, // kilocode_change KILO_CHANNEL: `'${Script.channel}'`, }, files: { diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts old mode 100755 new mode 100644 index a23760055b3..c6171f9d3b3 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -7,7 +7,6 @@ import path from "path" import { fileURLToPath } from "url" import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin" import { createRequire } from "module" // kilocode_change -import { prepareModelsSnapshot } from "./kilocode/models-snapshot" // kilocode_change const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -16,9 +15,15 @@ const require = createRequire(import.meta.url) // kilocode_change process.chdir(dir) +const generated = await import("./generate.ts") + import { Script } from "@opencode-ai/script" import pkg from "../package.json" -import { LanceDBRuntime } from "../src/kilocode/lancedb" // kilocode_change +// kilocode_change start +import { stageBubblewrap } from "./kilocode/bubblewrap" +import { LanceDBRuntime } from "../src/kilocode/lancedb" +import { KiloSandboxWorker } from "./kilocode/kilo-sandbox-worker" +// kilocode_change end // Load migrations from migration directories const migrationDirs = ( @@ -55,7 +60,6 @@ const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") const sourcemapsFlag = process.argv.includes("--sourcemaps") const plugin = createSolidTransformPlugin() -// kilocode_change - packages/app was removed; the web UI embed step is no longer applicable // kilocode_change start - codebase indexing async function copyTreeSitterWasms(outputDir: string) { @@ -75,9 +79,7 @@ async function copyTreeSitterWasms(outputDir: string) { console.log(`copied ${languageWasmFiles.length + 1} tree-sitter wasm files to ${targetDir}`) } -// kilocode_change end -// kilocode_change start - embed Kilo Console static assets async function buildKiloConsole() { const app = path.resolve(dir, "../kilo-console") const out = path.join(app, "dist") @@ -100,9 +102,7 @@ async function copyKiloConsole(input: string, outputDir: string) { await fs.promises.cp(input, target, { recursive: true }) console.log(`copied Kilo Console assets to ${target}`) } -// kilocode_change end -// kilocode_change start - validate compiled binaries load the sidecar models snapshot function smokeEnv(root: string) { const env = { ...process.env } delete env.KILO_MODELS_PATH @@ -127,16 +127,14 @@ async function smokeModels(binaryPath: string) { try { const out = await $`${binaryPath} --pure models anthropic`.env(smokeEnv(root)).text() if (out.split(/\r?\n/).some((line) => line.startsWith("anthropic/"))) return - throw new Error("Compiled binary did not list Anthropic models from the sidecar snapshot") + throw new Error("Compiled binary did not list Anthropic models from the embedded snapshot") } finally { await fs.promises .rm(root, { recursive: true, force: true }) .catch((err) => console.warn(`Failed to remove smoke test directory ${root}`, err)) } } -// kilocode_change end -// kilocode_change start - upstream's createEmbeddedWebUIBundle is intentionally removed because // Kilo dropped the packages/app web UI. Kept here as a commented reference so future upstream merges // can see the deliberate divergence rather than treating a re-add as a clean re-introduction. // const createEmbeddedWebUIBundle = async () => { @@ -248,15 +246,11 @@ const targets = singleFlag }) : allTargets -// kilocode_change start - prepare one validated models snapshot before any target compile -const snapshot = await prepareModelsSnapshot() -console.log( - `Prepared models snapshot from ${snapshot.source} (${snapshot.providers} providers, ${snapshot.models} models)`, -) -// kilocode_change end - await $`rm -rf dist` -const kiloConsoleDist = await buildKiloConsole() // kilocode_change +// kilocode_change start +const kiloConsoleDist = await buildKiloConsole() +const kiloSandboxWorker = await KiloSandboxWorker.bundle() +// kilocode_change end const binaries: Record = {} if (!skipInstall) { @@ -277,13 +271,21 @@ for (const item of targets) { console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` + // kilocode_change start + const bwrap = + item.os === "linux" && process.env.KILO_SKIP_BUNDLED_BWRAP !== "1" + ? await stageBubblewrap(item.arch, path.resolve(dir, `dist/${name}/bin`)) + : undefined + // kilocode_change end const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js") const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) const workerPath = "./src/cli/cmd/tui/worker.ts" - const sessionExportWorkerPath = "./src/kilocode/session-export/worker.ts" // kilocode_change - const indexingWorkerPath = "./src/kilocode/indexing-worker.ts" // kilocode_change + // kilocode_change start + const sessionExportWorkerPath = "./src/kilocode/session-export/worker.ts" + const indexingWorkerPath = "./src/kilocode/indexing-worker.ts" + // kilocode_change end // Use platform-specific bunfs root path based on target OS const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" @@ -295,8 +297,8 @@ for (const item of targets) { plugins: [plugin], // kilocode_change start - skip sourcemaps for release builds (each .js.map adds ~50 MB per target → ~600 MB total) sourcemap: Script.release ? "none" : "external", + external: ["node-gyp", ...LanceDBRuntime.external], // kilocode_change end - external: ["node-gyp", ...LanceDBRuntime.external], // kilocode_change format: "esm", minify: true, // kilocode_change start - disable code-splitting to avoid a Bun 1.3.14 codegen bug. @@ -314,8 +316,10 @@ for (const item of targets) { autoloadTsconfig: true, autoloadPackageJson: true, target: name.replace(pkg.name, "bun") as any, - outfile: `dist/${name}/bin/kilo`, // kilocode_change - execArgv: [`--user-agent=kilo/${Script.version}`, "--use-system-ca", "--"], // kilocode_change + // kilocode_change start + outfile: `dist/${name}/bin/kilo`, + execArgv: [`--user-agent=kilo/${Script.version}`, "--use-system-ca", "--"], + // kilocode_change end windows: {}, }, // kilocode_change start - packages/app was removed; no embedded web UI @@ -325,21 +329,28 @@ for (const item of targets) { define: { KILO_VERSION: `'${Script.version}'`, KILO_MIGRATIONS: JSON.stringify(migrations), + KILO_MODELS_DEV: generated.modelsData, OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, KILO_WORKER_PATH: workerPath, - KILO_SESSION_EXPORT_WORKER_PATH: sessionExportWorkerPath, // kilocode_change - KILO_INDEXING_WORKER_PATH: indexingWorkerPath, // kilocode_change + // kilocode_change start + KILO_SESSION_EXPORT_WORKER_PATH: sessionExportWorkerPath, + KILO_INDEXING_WORKER_PATH: indexingWorkerPath, + KILO_SANDBOX_MUTATION_WORKER_PATH: JSON.stringify(KiloSandboxWorker.filename), + // kilocode_change end KILO_CHANNEL: `'${Script.channel}'`, KILO_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", - KILO_BUILD_KIND: Script.release ? `'release'` : `'source'`, // kilocode_change + // kilocode_change start + KILO_BWRAP_SHA256: bwrap ? `'${bwrap}'` : "undefined", + KILO_BUILD_KIND: Script.release ? `'release'` : `'source'`, + // kilocode_change end }, }) - await fs.promises.copyFile(snapshot.path, path.resolve(dir, `dist/${name}/bin/models-snapshot.json`)) // kilocode_change - await copyTreeSitterWasms(path.resolve(dir, `dist/${name}/bin`)) // kilocode_change - await copyKiloConsole(kiloConsoleDist, path.resolve(dir, `dist/${name}/bin`)) // kilocode_change + // kilocode_change start + await copyTreeSitterWasms(path.resolve(dir, `dist/${name}/bin`)) + await copyKiloConsole(kiloConsoleDist, path.resolve(dir, `dist/${name}/bin`)) + await KiloSandboxWorker.copy(kiloSandboxWorker, path.resolve(dir, `dist/${name}/bin`)) - // kilocode_change start - fix Nix-specific ELF interpreter paths for Linux binaries if (item.os === "linux") { const interpreters: Record = { x64: "/lib64/ld-linux-x86-64.so.2", @@ -367,27 +378,46 @@ for (const item of targets) { try { const versionOutput = await $`${binaryPath} --version`.text() console.log(`Smoke test passed: ${versionOutput.trim()}`) + // kilocode_change start console.log(`Running smoke test: ${binaryPath} --pure models anthropic`) await smokeModels(binaryPath) console.log("Models snapshot smoke test passed") + await KiloSandboxWorker.smoke(binaryPath) + console.log("Kilo sandbox mutation worker smoke test passed") + // kilocode_change end + // kilocode_change start } catch (e) { console.error(`Smoke test failed for ${name}:`, e) process.exit(1) } } + // kilocode_change end await $`rm -rf ./dist/${name}/bin/tui` + // kilocode_change start + if (bwrap) { + const licenses = path.resolve(dir, `dist/${name}/bin/licenses/bubblewrap`) + const content = await Promise.all([ + Bun.file(path.resolve(dir, "../../LICENSE")).text(), + Bun.file(path.join(licenses, "NOTICE")).text(), + Bun.file(path.join(licenses, "COPYING")).text(), + Bun.file(path.join(licenses, "MUSL-COPYRIGHT")).text(), + ]) + await Bun.write(`dist/${name}/LICENSE`, content.join("\n\n---\n\n")) + } + // kilocode_change end await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( { name, version: Script.version, + license: bwrap ? "SEE LICENSE IN LICENSE" : pkg.license, // kilocode_change preferUnplugged: true, os: [item.os], cpu: [item.arch], - keywords: pkg.keywords, // kilocode_change - private: pkg.private, // kilocode_change // kilocode_change start + keywords: pkg.keywords, + private: pkg.private, repository: { type: "git", url: "https://github.com/Kilo-Org/kilocode", @@ -406,13 +436,17 @@ if (Script.release) { for (const key of Object.keys(binaries)) { const archive = key.replace(pkg.name, "kilo") // kilocode_change if (key.includes("linux")) { - const out = path.resolve("dist", `${archive}.tar.gz`) // kilocode_change - await $`tar -czf ${out} *`.cwd(`dist/${key}/bin`) // kilocode_change - archives.push(out) // kilocode_change + // kilocode_change start + const out = path.resolve("dist", `${archive}.tar.gz`) + await $`tar -czf ${out} *`.cwd(`dist/${key}/bin`) + archives.push(out) + // kilocode_change end } else { - const out = path.resolve("dist", `${archive}.zip`) // kilocode_change - await $`zip -r ${out} *`.cwd(`dist/${key}/bin`) // kilocode_change - archives.push(out) // kilocode_change + // kilocode_change start + const out = path.resolve("dist", `${archive}.zip`) + await $`zip -r ${out} *`.cwd(`dist/${key}/bin`) + archives.push(out) + // kilocode_change end } } await $`gh release upload v${Script.version} ${archives} --clobber` // kilocode_change diff --git a/packages/opencode/script/generate.ts b/packages/opencode/script/generate.ts index 69015c6ffb2..6a187581890 100644 --- a/packages/opencode/script/generate.ts +++ b/packages/opencode/script/generate.ts @@ -1,6 +1,6 @@ import path from "path" import { fileURLToPath } from "url" -import { prepareModelsSnapshot } from "./kilocode/models-snapshot" // kilocode_change +import { parseModelsSnapshot } from "../src/kilocode/provider/models-snapshot-shape" // kilocode_change const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -8,7 +8,11 @@ const dir = path.resolve(__dirname, "..") process.chdir(dir) +const modelsUrl = process.env.KILO_MODELS_URL || "https://models.dev" // kilocode_change start -const snapshot = await prepareModelsSnapshot() -console.log(`Generated models-snapshot.json (${snapshot.providers} providers, ${snapshot.models} models)`) +const raw = process.env.MODELS_DEV_API_JSON + ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() + : await fetch(`${modelsUrl}/api.json`).then((x) => x.text()) +export const modelsData = JSON.stringify(parseModelsSnapshot(raw).data) // kilocode_change end +console.log("Loaded models.dev snapshot") diff --git a/packages/opencode/script/kilocode/bubblewrap.ts b/packages/opencode/script/kilocode/bubblewrap.ts new file mode 100644 index 00000000000..d6f5aa779a7 --- /dev/null +++ b/packages/opencode/script/kilocode/bubblewrap.ts @@ -0,0 +1,177 @@ +import { createHash } from "node:crypto" +import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs" +import os from "node:os" +import path from "node:path" + +const version = "0.11.2" +const commit = "1b80120ef26a28e065e67f89bfef873f13bdd317" +const sourceUrl = `https://codeload.github.com/containers/bubblewrap/tar.gz/${commit}` +const sourceSha256 = "55a1f42de8f62f6cd8cc414229ce166ec6128ca4386b8c25dfca4229e44b56aa" +const cache = process.env.KILO_BWRAP_CACHE ?? path.join(os.tmpdir(), "kilo-bubblewrap", commit) + +const capability = `#pragma once +#include +#include +#include +#include + +typedef struct __user_cap_header_struct *cap_user_header_t; +typedef struct __user_cap_data_struct *cap_user_data_t; +typedef int cap_value_t; + +static inline int capget(cap_user_header_t header, cap_user_data_t data) { + return (int) syscall(SYS_capget, header, data); +} + +static inline int capset(cap_user_header_t header, const cap_user_data_t data) { + return (int) syscall(SYS_capset, header, data); +} + +static inline int cap_from_name(const char *name, cap_value_t *cap) { + (void) name; + (void) cap; + errno = EINVAL; + return -1; +} +` + +const config = `#pragma once +#define PACKAGE_STRING "bubblewrap ${version} for Kilo" +#define PACKAGE_VERSION "${version}" +` + +const notice = `Bubblewrap ${version} + +SPDX-License-Identifier: LGPL-2.0-or-later +Source: https://github.com/containers/bubblewrap/tree/${commit} + +Kilo distributes Bubblewrap as a separate executable. The complete license text is +in COPYING. The exact corresponding source is in bubblewrap-${commit}.tar.gz, and +the build recipe and generated compatibility headers are in build.ts. + +The executable is statically linked with musl. Its copyright and license notices +are in MUSL-COPYRIGHT. +` + +function sha256(file: string) { + return createHash("sha256").update(readFileSync(file)).digest("hex") +} + +async function source() { + const archive = path.join(cache, `bubblewrap-${commit}.tar.gz`) + if (!existsSync(archive) || sha256(archive) !== sourceSha256) { + mkdirSync(cache, { recursive: true }) + const response = await fetch(sourceUrl) + if (!response.ok) throw new Error(`Could not download Bubblewrap source: ${response.status}`) + await Bun.write(archive, response) + if (sha256(archive) !== sourceSha256) throw new Error("Bubblewrap source digest mismatch") + } + + const root = path.join(cache, `bubblewrap-${commit}`) + if (!existsSync(root)) { + const proc = Bun.spawn(["tar", "-xzf", archive, "-C", cache], { stdout: "inherit", stderr: "inherit" }) + if ((await proc.exited) !== 0) throw new Error("Could not extract Bubblewrap source") + } + return { archive, root } +} + +function target(arch: "x64" | "arm64") { + return arch === "x64" ? "x86_64-linux-musl" : "aarch64-linux-musl" +} + +function muslLicense(zig: string) { + const result = Bun.spawnSync([zig, "env"]) + if (result.exitCode !== 0) throw new Error("Could not inspect the Zig toolchain") + const match = result.stdout.toString().match(/(?:"lib_dir"\s*:\s*|\.lib_dir\s*=\s*)"([^"]+)"/) + if (!match) throw new Error("Could not locate Zig's bundled musl license") + const license = path.join(match[1], "libc", "musl", "COPYRIGHT") + if (!existsSync(license)) throw new Error(`Zig's bundled musl license is missing at ${license}`) + return license +} + +async function compile(arch: "x64" | "arm64") { + const sourceTree = await source() + const out = path.join(cache, `bwrap-${arch}`) + const include = path.join(cache, "include", "sys") + const generated = path.join(cache, "generated") + mkdirSync(include, { recursive: true }) + mkdirSync(generated, { recursive: true }) + await Bun.write(path.join(include, "capability.h"), capability) + await Bun.write(path.join(generated, "config.h"), config) + + const zig = process.env.ZIG ?? "zig" + const args = [ + zig, + "cc", + "-target", + target(arch), + "-static", + "-fPIE", + "-pie", + "-s", + "-O2", + "-D_GNU_SOURCE", + "-I", + path.join(cache, "include"), + "-I", + generated, + "-I", + sourceTree.root, + path.join(sourceTree.root, "bubblewrap.c"), + path.join(sourceTree.root, "bind-mount.c"), + path.join(sourceTree.root, "network.c"), + path.join(sourceTree.root, "utils.c"), + "-o", + out, + ] + const proc = Bun.spawn(args, { stdout: "inherit", stderr: "inherit" }) + if ((await proc.exited) !== 0) throw new Error(`Could not build Bubblewrap for Linux ${arch}`) + chmodSync(out, 0o755) + + return { + executable: out, + digest: sha256(out), + archive: sourceTree.archive, + license: path.join(sourceTree.root, "COPYING"), + musl: muslLicense(zig), + } +} + +const builds = new Map<"x64" | "arm64", ReturnType>() + +export function buildBubblewrap(arch: "x64" | "arm64") { + const cached = builds.get(arch) + if (cached) return cached + const built = compile(arch) + builds.set(arch, built) + return built +} + +export async function stageBubblewrap(arch: "x64" | "arm64", dir: string) { + const built = await buildBubblewrap(arch) + const licenses = path.join(dir, "licenses", "bubblewrap") + mkdirSync(dir, { recursive: true }) + rmSync(licenses, { recursive: true, force: true }) + mkdirSync(licenses, { recursive: true }) + copyFileSync(built.executable, path.join(dir, "bwrap")) + await Bun.write(path.join(licenses, "NOTICE"), notice) + copyFileSync(built.license, path.join(licenses, "COPYING")) + copyFileSync(built.musl, path.join(licenses, "MUSL-COPYRIGHT")) + copyFileSync(built.archive, path.join(licenses, `bubblewrap-${commit}.tar.gz`)) + copyFileSync(import.meta.path, path.join(licenses, "build.ts")) + chmodSync(path.join(dir, "bwrap"), 0o755) + return built.digest +} + +if (import.meta.main) { + const arch = process.argv[process.argv.indexOf("--arch") + 1] + const output = process.argv[process.argv.indexOf("--output") + 1] + if ((arch !== "x64" && arch !== "arm64") || !output) { + throw new Error("Usage: bun bubblewrap.ts --arch --output ") + } + const built = await buildBubblewrap(arch) + mkdirSync(path.dirname(output), { recursive: true }) + copyFileSync(built.executable, output) + chmodSync(output, 0o755) + console.log(`${output} sha256:${built.digest}`) +} diff --git a/packages/opencode/script/kilocode/kilo-sandbox-worker.ts b/packages/opencode/script/kilocode/kilo-sandbox-worker.ts new file mode 100644 index 00000000000..c82cc5b9f14 --- /dev/null +++ b/packages/opencode/script/kilocode/kilo-sandbox-worker.ts @@ -0,0 +1,69 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +export namespace KiloSandboxWorker { + export const filename = "kilo-sandbox-mutation-worker.js" + + export async function bundle() { + const result = await Bun.build({ + entrypoints: ["../kilo-sandbox/src/kilo-sandbox-mutation-worker.ts"], + target: "bun", + format: "esm", + minify: true, + }) + if (!result.success || result.outputs.length !== 1) throw new Error("Could not bundle Kilo sandbox mutation worker") + return result.outputs[0] + } + + export async function copy(worker: Blob, dir: string) { + const target = path.join(dir, filename) + await Bun.write(target, worker) + console.log(`copied Kilo sandbox mutation worker to ${target}`) + } + + export async function smoke(binary: string) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-sandbox-worker-")) + const target = path.join(root, "value.txt") + const worker = path.join(path.dirname(binary), filename) + try { + const proc = Bun.spawn([binary, worker], { + env: { ...process.env, BUN_BE_BUN: "1" }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + windowsHide: true, + }) + await proc.stdin.write( + JSON.stringify({ + op: "batch", + operations: [ + { op: "makeDirectory", path: root, options: { recursive: true } }, + { op: "writeFileString", path: target, data: "worker" }, + ], + }), + ) + await proc.stdin.end() + const [stdout, stderr, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]) + if (code !== 0) throw new Error(stderr || `Kilo sandbox mutation worker exited ${code}`) + const response: unknown = JSON.parse(stdout) + if ( + typeof response !== "object" || + response === null || + !("ok" in response) || + response.ok !== true || + (await Bun.file(target).text()) !== "worker" + ) { + throw new Error("Packaged Kilo sandbox mutation worker did not write the expected content") + } + } finally { + await fs + .rm(root, { recursive: true, force: true }) + .catch((err) => console.warn(`Failed to remove Kilo sandbox worker smoke test directory ${root}`, err)) + } + } +} diff --git a/packages/opencode/script/kilocode/models-snapshot.ts b/packages/opencode/script/kilocode/models-snapshot.ts deleted file mode 100644 index 81a81431164..00000000000 --- a/packages/opencode/script/kilocode/models-snapshot.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs/promises" -import path from "path" -import { parseModelsSnapshot } from "../../src/kilocode/provider/models-snapshot-shape" - -export const MODELS_SNAPSHOT_PATH = path.resolve(import.meta.dir, "../../src/provider/models-snapshot.json") - -const STALE = [ - path.resolve(import.meta.dir, "../../src/provider/models-snapshot.js"), - path.resolve(import.meta.dir, "../../src/provider/models-snapshot.d.ts"), -] - -type Fetcher = (input: string, init?: RequestInit) => Promise - -export interface PrepareModelsSnapshotOptions { - input?: string - output?: string - source?: string - fetcher?: Fetcher -} - -async function content(opts: PrepareModelsSnapshotOptions) { - const input = opts.input ?? process.env.MODELS_DEV_API_JSON - if (input) { - return { - source: input, - text: await Bun.file(input).text(), - } - } - - const base = opts.source ?? process.env.KILO_MODELS_URL ?? "https://models.dev" - const url = `${base.replace(/\/+$/, "")}/api.json` - const fetcher = opts.fetcher ?? fetch - const res = await fetcher(url, { signal: AbortSignal.timeout(20_000) }) - if (!res.ok) throw new Error(`Failed to download models snapshot from ${url}: ${res.status} ${res.statusText}`) - return { - source: url, - text: await res.text(), - } -} - -async function replace(file: string, body: string) { - await fs.mkdir(path.dirname(file), { recursive: true }) - const tmp = path.join(path.dirname(file), `.models-snapshot.${process.pid}.${Date.now()}.tmp`) - try { - await fs.writeFile(tmp, body) - await fs.rename(tmp, file) - } catch (err) { - await fs - .rm(tmp, { force: true }) - .catch((cause) => console.warn(`Failed to remove temporary models snapshot ${tmp}`, cause)) - throw err - } -} - -async function cleanup(file: string) { - if (path.resolve(file) !== MODELS_SNAPSHOT_PATH) return - await Promise.all( - STALE.map((item) => - fs.rm(item, { force: true }).catch((err) => console.warn(`Failed to remove stale models snapshot ${item}`, err)), - ), - ) -} - -export async function prepareModelsSnapshot(opts: PrepareModelsSnapshotOptions = {}) { - const out = path.resolve(opts.output ?? MODELS_SNAPSHOT_PATH) - const raw = await content(opts) - const parsed = parseModelsSnapshot(raw.text, raw.source) - await replace(out, `${JSON.stringify(parsed.data)}\n`) - await cleanup(out) - return { - path: out, - source: raw.source, - providers: parsed.stats.providers, - models: parsed.stats.models, - } -} diff --git a/packages/opencode/script/kilocode/test-profile.ts b/packages/opencode/script/kilocode/test-profile.ts new file mode 100644 index 00000000000..9851b2f4a0b --- /dev/null +++ b/packages/opencode/script/kilocode/test-profile.ts @@ -0,0 +1,101 @@ +export namespace TestProfile { + // Broad globs keep platform coverage maintainable as tests are added or renamed. + // Full macOS runs on main remain the backstop for tests outside these areas. + const profiles = { + darwin: { + description: "Darwin-native process, terminal, filesystem, worktree, and runtime coverage", + groups: { + cli: [ + "cli/acp/*.test.ts", + "cli/run/{footer.view,run-process,scrollback.surface}.test.{ts,tsx}", + "cli/serve/*.test.ts", + "cli/smokes/*.test.ts", + "cli/tui/{app-lifecycle,dialog-prompt,diff-viewer-file-tree,diff-viewer,inline-tool-wrap-snapshot,keymap,plugin-loader-entrypoint,slot-replace,thread,use-event}.test.{ts,tsx}", + ], + filesystem: [ + "file/{index,path-traversal,ripgrep}.test.ts", + "git/*.test.ts", + "image/*.test.ts", + "plugin/{install-concurrency,loader-shared}.test.ts", + "reference/*.test.ts", + "snapshot/*.test.ts", + "tool/{external-directory,glob,grep,read,repo_clone,repo_overview,shell}.test.ts", + "util/{filesystem,module,process,which}.test.ts", + ], + kilo: [ + "kilocode/{background-process,bin-tree-sitter-env,daemon,external-directory-boundary,indexing-worker,indexing-worktree,mcp-oauth-callback,primary-worktree,snapshot-freeze-repro,snapshot-revert-move,snapshot-seed}.test.ts", + "kilocode/cli/install-artifact.test.ts", + "kilocode/sandbox/*.test.ts", + "kilocode/server/{listener-runtime,worktree-list}.test.ts", + "kilocode/session-export/{e2e,sequence,worker,workspace-provider}.test.ts", + "kilocode/session-export/worker/{storage,zstd}.test.ts", + "kilocode/sessions/*.test.ts", + "kilocode/worktree*.test.ts", + ], + process: ["provider/header-timeout.test.ts", "session/{prompt,retry}.test.ts", "shell/*.test.ts"], + project: ["project/*.test.ts"], + pty: ["pty/pty-*.test.ts", "server/httpapi-pty*.test.ts"], + server: [ + "server/{httpapi-compression,httpapi-experimental,httpapi-file,httpapi-listen,httpapi-workspace-routing,project-init-git,workspace-proxy,worktree-endpoint-repro}.test.ts", + ], + }, + }, + } as const + + export const names = Object.keys(profiles) + + export function resolve(name: string, all: readonly string[]) { + const files = all.map((file) => file.replaceAll("\\", "/")) + const profile = profiles[name as keyof typeof profiles] + if (!profile) { + return { + ok: false as const, + error: `Unknown test profile "${name}". Available profiles: ${names.join(", ")}`, + } + } + + const groups = Object.entries(profile.groups) + const patterns = groups.flatMap(([, patterns]) => patterns) + const malformed = patterns.filter( + (pattern) => + pattern.startsWith("/") || + pattern.startsWith("test/") || + pattern.includes("\\") || + pattern.split("/").includes("..") || + !/\.test\.(ts|tsx|\{ts,tsx\})$/.test(pattern), + ) + const seen = new Set() + const duplicates = patterns.filter((pattern) => { + if (seen.has(pattern)) return true + seen.add(pattern) + return false + }) + const unsorted = groups + .filter(([, patterns]) => + patterns.some((pattern, index) => index > 0 && patterns[index - 1].localeCompare(pattern) > 0), + ) + .map(([group]) => group) + const globs = patterns.map((pattern) => ({ pattern, glob: new Bun.Glob(pattern) })) + const unmatched = globs.filter((item) => !files.some((file) => item.glob.match(file))).map((item) => item.pattern) + const errors = [ + malformed.length > 0 ? `Malformed patterns: ${malformed.join(", ")}` : "", + duplicates.length > 0 ? `Duplicate patterns: ${duplicates.join(", ")}` : "", + unmatched.length > 0 ? `Unmatched patterns: ${unmatched.join(", ")}` : "", + unsorted.length > 0 ? `Unsorted groups: ${unsorted.join(", ")}` : "", + patterns.length === 0 ? "Profile contains no patterns" : "", + ].filter(Boolean) + + if (errors.length > 0) { + return { + ok: false as const, + error: `Invalid test profile "${name}":\n${errors.map((error) => `- ${error}`).join("\n")}`, + } + } + + return { + ok: true as const, + description: profile.description, + files: files.filter((file) => globs.some((item) => item.glob.match(file))), + } + } +} diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index d158c645259..2b811968ce1 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -137,6 +137,23 @@ function copyResources(source) { fs.rmSync(target, { recursive: true, force: true }) fs.cpSync(dir, target, { recursive: true }) } + + const bwrap = path.join(path.dirname(source), "bwrap") + if (fs.existsSync(bwrap)) { + const target = path.join(__dirname, "bin", "bwrap") + fs.copyFileSync(bwrap, target) + fs.chmodSync(target, 0o755) + } + + const licenses = path.join(path.dirname(source), "licenses") + if (fs.existsSync(licenses)) { + const target = path.join(__dirname, "bin", "licenses") + fs.rmSync(target, { recursive: true, force: true }) + fs.cpSync(licenses, target, { recursive: true }) + } + + const worker = path.join(path.dirname(source), "kilo-sandbox-mutation-worker.js") + if (fs.existsSync(worker)) fs.copyFileSync(worker, path.join(__dirname, "bin", "kilo-sandbox-mutation-worker.js")) } function copyBinary(source) { diff --git a/packages/opencode/script/profile-test-files.ts b/packages/opencode/script/profile-test-files.ts new file mode 100644 index 00000000000..12b9bccd84b --- /dev/null +++ b/packages/opencode/script/profile-test-files.ts @@ -0,0 +1,42 @@ +// Per-file profiler for finding candidate test-speed work; see ../../perf/test-suite.md +// for the benchmark notes, kept wins, and discarded experiments. +// Example: TEST_PROFILE_GLOB='test/server/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile:test +const pattern = Bun.env.TEST_PROFILE_GLOB ?? "test/**/*.test.{ts,tsx}" +const limit = Number(Bun.env.TEST_PROFILE_LIMIT ?? 0) +const timeout = Bun.env.TEST_PROFILE_TIMEOUT ?? "30000" +const files = Array.fromAsync(new Bun.Glob(pattern).scan({ cwd: import.meta.dir + "/..", onlyFiles: true })) + .then((files) => files.toSorted()) + .then((files) => (limit > 0 ? files.slice(0, limit) : files)) + +const results = [] +for (const file of await files) { + const start = performance.now() + const proc = Bun.spawn(["bun", "test", "--timeout", timeout, file], { + cwd: import.meta.dir + "/..", + stdout: "pipe", + stderr: "pipe", + env: Bun.env, + }) + const [output, error, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]) + const seconds = (performance.now() - start) / 1000 + results.push({ file, seconds, exitCode }) + console.log(`${exitCode === 0 ? "PASS" : "FAIL"} ${seconds.toFixed(3)}s ${file}`) + if (exitCode !== 0) console.log((output + error).trim()) +} + +const sorted = results.toSorted((a, b) => b.seconds - a.seconds) +console.log("\nSlowest test files:") +for (const result of sorted.slice(0, Number(Bun.env.TEST_PROFILE_TOP ?? 20))) { + console.log(`${result.seconds.toFixed(3)}s ${result.exitCode === 0 ? "PASS" : "FAIL"} ${result.file}`) +} + +if (sorted[0]) { + console.log(`METRIC slowest_test_file_seconds=${sorted[0].seconds.toFixed(3)}`) + console.log(`METRIC profiled_test_files=${results.length}`) +} + +if (results.some((result) => result.exitCode !== 0)) process.exit(1) diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts old mode 100755 new mode 100644 index 0d9ab24e2d5..67cfc3ee427 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -105,7 +105,7 @@ if (!Script.preview) { "pkgdesc='The AI coding agent built for the terminal.'", "url='https://github.com/Kilo-Org/kilocode'", "arch=('aarch64' 'x86_64')", - "license=('MIT')", + "license=('MIT' 'LGPL-2.0-or-later')", // kilocode_change "provides=('kilo')", "conflicts=('kilo')", "depends=('ripgrep')", @@ -118,8 +118,11 @@ if (!Script.preview) { "", "package() {", ' install -Dm755 ./kilo "${pkgdir}/usr/lib/kilo/kilo"', // kilocode_change - ' install -dm755 "${pkgdir}/usr/bin" "${pkgdir}/usr/lib/kilo/tree-sitter"', // kilocode_change + ' install -Dm755 ./bwrap "${pkgdir}/usr/lib/kilo/bwrap"', // kilocode_change + ' install -Dm644 ./kilo-sandbox-mutation-worker.js "${pkgdir}/usr/lib/kilo/kilo-sandbox-mutation-worker.js"', // kilocode_change + ' install -dm755 "${pkgdir}/usr/bin" "${pkgdir}/usr/lib/kilo/tree-sitter" "${pkgdir}/usr/share/licenses/kilo"', // kilocode_change ' cp -r ./tree-sitter/. "${pkgdir}/usr/lib/kilo/tree-sitter/"', // kilocode_change + ' cp -r ./licenses/. "${pkgdir}/usr/share/licenses/kilo/"', // kilocode_change " printf '%s\\n' '#!/bin/sh' 'export KILO_TREE_SITTER_WASM_DIR=/usr/lib/kilo/tree-sitter' 'exec /usr/lib/kilo/kilo \"$@\"' > \"${pkgdir}/usr/bin/kilo\"", // kilocode_change ' chmod 755 "${pkgdir}/usr/bin/kilo"', // kilocode_change "}", @@ -164,7 +167,7 @@ if (!Script.preview) { ` sha256 "${macX64Sha}"`, "", " def install", - ' libexec.install "kilo", "tree-sitter"', // kilocode_change + ' libexec.install "kilo", "kilo-sandbox-mutation-worker.js", "tree-sitter"', // kilocode_change ' (bin/"kilo").write_env_script libexec/"kilo", KILO_TREE_SITTER_WASM_DIR: libexec/"tree-sitter"', // kilocode_change " end", " end", @@ -173,7 +176,7 @@ if (!Script.preview) { ` sha256 "${macArm64Sha}"`, "", " def install", - ' libexec.install "kilo", "tree-sitter"', // kilocode_change + ' libexec.install "kilo", "kilo-sandbox-mutation-worker.js", "tree-sitter"', // kilocode_change ' (bin/"kilo").write_env_script libexec/"kilo", KILO_TREE_SITTER_WASM_DIR: libexec/"tree-sitter"', // kilocode_change " end", " end", @@ -184,7 +187,7 @@ if (!Script.preview) { ` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-linux-x64.tar.gz"`, ` sha256 "${x64Sha}"`, " def install", - ' libexec.install "kilo", "tree-sitter"', // kilocode_change + ' libexec.install "kilo", "bwrap", "kilo-sandbox-mutation-worker.js", "tree-sitter", "licenses"', // kilocode_change ' (bin/"kilo").write_env_script libexec/"kilo", KILO_TREE_SITTER_WASM_DIR: libexec/"tree-sitter"', // kilocode_change " end", " end", @@ -192,7 +195,7 @@ if (!Script.preview) { ` url "https://github.com/Kilo-Org/kilocode/releases/download/v${Script.version}/kilo-linux-arm64.tar.gz"`, ` sha256 "${arm64Sha}"`, " def install", - ' libexec.install "kilo", "tree-sitter"', // kilocode_change + ' libexec.install "kilo", "bwrap", "kilo-sandbox-mutation-worker.js", "tree-sitter", "licenses"', // kilocode_change ' (bin/"kilo").write_env_script libexec/"kilo", KILO_TREE_SITTER_WASM_DIR: libexec/"tree-sitter"', // kilocode_change " end", " end", diff --git a/packages/opencode/script/test-runner.ts b/packages/opencode/script/test-runner.ts index 7ee61e1ed9b..d5551a500ec 100644 --- a/packages/opencode/script/test-runner.ts +++ b/packages/opencode/script/test-runner.ts @@ -7,6 +7,7 @@ import os from "os" import path from "path" import fs from "fs/promises" +import { TestProfile } from "./kilocode/test-profile" const root = path.resolve(import.meta.dir, "..") const argv = process.argv.slice(2) @@ -29,6 +30,7 @@ if (argv.includes("--help") || argv.includes("-h")) { " --timeout Per-test timeout passed to bun test (default: 60000)", " --file-timeout Per-file process timeout (default: 300000)", " --retries Extra attempts for failing files (default: 1)", + " --profile Run a curated test profile (env: KILO_TEST_PROFILE)", " --bail Stop on first failure", " --verbose Show full output for every file", " -h, --help Show this help", @@ -50,6 +52,15 @@ function opt(name: string, fallback: number) { return i >= 0 && i + 1 < argv.length ? Number(argv[i + 1]) || fallback : fallback } +function text(name: string) { + const i = argv.indexOf(`--${name}`) + if (i < 0) return + const value = argv[i + 1] + if (value && !value.startsWith("-")) return value + console.error(`Missing value for --${name}`) + process.exit(2) +} + const ci = argv.includes("--ci") const bail = argv.includes("--bail") const verbose = argv.includes("--verbose") @@ -60,8 +71,15 @@ const concurrency = opt("concurrency", Math.min(4, os.cpus().length)) const timeout = opt("timeout", 60000) const deadline = opt("file-timeout", 300000) const retries = opt("retries", 1) +const flag = text("profile") +const env = process.env.KILO_TEST_PROFILE?.trim() || undefined +if (flag && env && flag !== env) { + console.error(`Conflicting test profiles: --profile=${flag}, KILO_TEST_PROFILE=${env}`) + process.exit(2) +} +const profile = flag ?? env -const valued = new Set(["--concurrency", "--timeout", "--file-timeout", "--retries"]) +const valued = new Set(["--concurrency", "--timeout", "--file-timeout", "--retries", "--profile"]) const patterns = argv.filter((arg, i) => { if (arg.startsWith("-")) return false if (i > 0 && valued.has(argv[i - 1])) return false @@ -84,7 +102,9 @@ const bold = (s: string) => (tty ? `\x1b[1m${s}\x1b[0m` : s) // --------------------------------------------------------------------------- const glob = new Bun.Glob("**/*.test.{ts,tsx}") -const all = (await Array.fromAsync(glob.scan({ cwd: path.join(root, "test") }))).sort() +const all = (await Array.fromAsync(glob.scan({ cwd: path.join(root, "test") }))) + .map((file) => file.replaceAll("\\", "/")) + .sort() export const skipped = new Set([ // Upstream browser OAuth integration tests bind the fixed callback port and @@ -92,9 +112,28 @@ export const skipped = new Set([ "mcp/oauth-browser.test.ts", ]) +const selected = (() => { + if (!profile) return all + const result = TestProfile.resolve(profile, all) + if (!result.ok) { + console.error(result.error) + process.exit(2) + } + const blocked = result.files.filter((file) => skipped.has(file)) + if (blocked.length > 0) { + console.error(`Test profile "${profile}" contains skipped files:\n${blocked.map((file) => `- ${file}`).join("\n")}`) + process.exit(2) + } + console.log(`Using test profile "${profile}": ${result.description} (${result.files.length} files)`) + return result.files +})() const matched = - patterns.length > 0 ? all.filter((f) => patterns.some((p) => f.includes(p) || path.join("test", f).includes(p))) : all -const files = patterns.length > 0 ? matched : matched.filter((f) => !skipped.has(f)) // kilocode_change + patterns.length > 0 + ? selected.filter((file) => + patterns.some((pattern) => file.includes(pattern) || path.join("test", file).includes(pattern)), + ) + : selected +const files = patterns.length > 0 && !profile ? matched : matched.filter((file) => !skipped.has(file)) // kilocode_change if (files.length === 0) { console.log("No test files found") diff --git a/packages/opencode/specs/tui-plugins.md b/packages/opencode/specs/tui-plugins.md index 67eb9122b5a..89ebc1ffbc8 100644 --- a/packages/opencode/specs/tui-plugins.md +++ b/packages/opencode/specs/tui-plugins.md @@ -230,6 +230,7 @@ Top-level API groups exposed to `tui(api, options, meta)`: - `api.attention.notify(input)` - `api.keys.formatSequence(parts)`, `formatBindings(bindings)` - `api.keymap` +- `api.mode.current()`, `api.mode.push(mode)` - `api.route.register(routes)` / `api.route.navigate(name, params?)` / `api.route.current` - `api.ui.Dialog`, `DialogAlert`, `DialogConfirm`, `DialogPrompt`, `DialogSelect`, `Slot`, `Prompt`, `ui.toast`, `ui.dialog` - `api.tuiConfig` @@ -255,6 +256,68 @@ Top-level API groups exposed to `tui(api, options, meta)`: - Disposers returned by `api.keymap` registrations and `acquireResource(...)` are automatically cleaned up when the plugin deactivates. You do not need to add those disposers to `api.lifecycle.onDispose(...)` yourself. - Built-in which-key shortcuts are resolved from flat `keybinds` command ids such as `which_key_toggle`, not plugin options. +#### Mode-aware layers + +Kilo registers a `mode` layer field on the host keymap. Plugins can use it to keep bindings active only in the relevant UI state. + +Built-in modes: + +- `base`: normal app, route, and prompt interaction. +- `modal`: host dialog stack is open, including dialogs rendered through `api.ui.dialog` and `api.ui.Dialog*` components. +- `autocomplete`: host prompt autocomplete is open. +- `api.mode.current()` returns the active top mode, or `base` when no pushed mode is active. + +Example: register a command and shortcut that are active only in normal app mode: + +```tsx +api.keymap.registerLayer({ + mode: "base", + commands: [ + { + name: "demo.open", + title: "Demo", + category: "Plugin", + namespace: "palette", + run() { + api.route.navigate("demo") + }, + }, + ], + bindings: [{ key: "ctrl+shift+m", cmd: "demo.open", desc: "Open demo" }], +}) +``` + +Layers without `mode` are not mode-gated and can remain active while dialogs or autocomplete are open. Use that only for intentionally global commands or low-level keymap extensions. + +Plugins that own a full-screen route or modal-like UI can temporarily push a plugin-specific mode with `api.mode.push(...)`. Use a plugin-scoped mode name. The returned disposer pops that specific stack entry and is idempotent, so popping an older mode while a newer mode is on top leaves the newer mode active. + +```tsx +import { onCleanup } from "solid-js" + +api.route.register([ + { + name: "demo", + render: () => { + const popMode = api.mode.push("acme.demo") + onCleanup(popMode) + + return ( + + demo + + ) + }, + }, +]) + +api.keymap.registerLayer({ + mode: "acme.demo", + bindings: [{ key: "escape", cmd: () => api.route.navigate("home"), desc: "Close demo" }], +}) +``` + +Mode pushes are automatically tracked by the plugin runtime. If a plugin is disabled, fails during activation, or the TUI shuts down before the plugin calls the disposer, Kilo pops the plugin's pushed modes during plugin cleanup. Calling the disposer yourself is still recommended for component lifetimes; cleanup remains idempotent. + ### Keys - `api.keys` exposes host-formatted shortcut display helpers for plugin UI. @@ -265,7 +328,7 @@ Top-level API groups exposed to `tui(api, options, meta)`: ### Attention - `api.attention.notify({ title?, message, notification?, sound? })` requests user attention while keeping terminal focus, notifications, and audio owned by the host. -- `message` is required; `title` defaults to `"opencode"`; `notification` defaults to enabled with `when: "blurred"`; `sound` defaults to enabled with `when: "always"`. +- `message` is required; `title` defaults to `"Kilo"`; `notification` defaults to enabled with `when: "blurred"`; `sound` defaults to enabled with `when: "always"`. - `when: "always"` requests delivery regardless of terminal focus state. - `when: "focused"` only requests delivery after the terminal is known focused; `when: "blurred"` only requests delivery after the terminal is known blurred. - Example: `notification: { when: "blurred" }, sound: { name: "question", when: "always" }` plays sound while focused but only triggers system notifications when blurred. diff --git a/packages/opencode/src/account/account.ts b/packages/opencode/src/account/account.ts index a0aed88cba8..2d855e0e952 100644 --- a/packages/opencode/src/account/account.ts +++ b/packages/opencode/src/account/account.ts @@ -1,4 +1,5 @@ import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" +import { serviceUse } from "@opencode-ai/core/effect/service-use" import { FetchHttpClient, HttpClient, @@ -181,6 +182,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Account") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts index 04380137c87..052f1cbd65a 100644 --- a/packages/opencode/src/account/repo.ts +++ b/packages/opencode/src/account/repo.ts @@ -1,4 +1,5 @@ import { eq } from "drizzle-orm" +import { serviceUse } from "@opencode-ai/core/effect/service-use" import { Effect, Layer, Option, Schema, Context } from "effect" import { Database } from "@/storage/db" @@ -38,6 +39,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/AccountRepo") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/acp/README.md b/packages/opencode/src/acp/README.md deleted file mode 100644 index ce9a22e03b5..00000000000 --- a/packages/opencode/src/acp/README.md +++ /dev/null @@ -1,174 +0,0 @@ -# ACP (Agent Client Protocol) Implementation - -This directory contains a clean, protocol-compliant implementation of the [Agent Client Protocol](https://agentclientprotocol.com/) for opencode. - -## Architecture - -The implementation follows a clean separation of concerns: - -### Core Components - -- **`agent.ts`** - Implements the `Agent` interface from `@agentclientprotocol/sdk` - - Handles initialization and capability negotiation - - Manages session lifecycle (`session/new`, `session/load`) - - Processes prompts and returns responses - - Properly implements ACP protocol v1 - -- **`client.ts`** - Implements the `Client` interface for client-side capabilities - - File operations (`readTextFile`, `writeTextFile`) - - Permission requests (auto-approves for now) - - Terminal support (stub implementation) - -- **`session.ts`** - Session state management - - Creates and tracks ACP sessions - - Maps ACP sessions to internal opencode sessions - - Maintains working directory context - - Handles MCP server configurations - -- **`server.ts`** - ACP server startup and lifecycle - - Sets up JSON-RPC over stdio using the official library - - Manages graceful shutdown on SIGTERM/SIGINT - - Provides Instance context for the agent - -- **`types.ts`** - Type definitions for internal use - -## Usage - -### Command Line - -```bash -# Start the ACP server in the current directory -opencode acp - -# Start in a specific directory -opencode acp --cwd /path/to/project -``` - -### Question Tool Opt-In - -ACP excludes `QuestionTool` by default. - -```bash -KILO_ENABLE_QUESTION_TOOL=1 opencode acp -``` - -Enable this only for ACP clients that support interactive question prompts. - -### Programmatic - -```typescript -import { ACPServer } from "./acp/server" - -await ACPServer.start() -``` - -### Integration with Zed - -Add to your Zed configuration (`~/.config/zed/settings.json`): - -```json -{ - "agent_servers": { - "OpenCode": { - "command": "opencode", - "args": ["acp"] - } - } -} -``` - -## Protocol Compliance - -This implementation follows the ACP specification v1: - -✅ **Initialization** - -- Proper `initialize` request/response with protocol version negotiation -- Capability advertisement (`agentCapabilities`) -- Authentication support (stub) - -✅ **Session Management** - -- `session/new` - Create new conversation sessions -- `session/load` - Resume existing sessions (basic support) -- Working directory context (`cwd`) -- MCP server configuration support - -✅ **Prompting** - -- `session/prompt` - Process user messages -- Content block handling (text, resources) -- Response with stop reasons - -✅ **Client Capabilities** - -- File read/write operations -- Permission requests -- Terminal support (stub for future) - -## Current Limitations - -### Not Yet Implemented - -1. **Streaming Responses** - Currently returns complete responses instead of streaming via `session/update` notifications -2. **Tool Call Reporting** - Doesn't report tool execution progress -3. **Session Modes** - No mode switching support yet -4. **Authentication** - No actual auth implementation -5. **Terminal Support** - Placeholder only -6. **Session Persistence** - `session/load` doesn't restore actual conversation history - -### Future Enhancements - -- **Real-time Streaming**: Implement `session/update` notifications for progressive responses -- **Tool Call Visibility**: Report tool executions as they happen -- **Session Persistence**: Save and restore full conversation history -- **Mode Support**: Implement different operational modes (ask, code, etc.) -- **Enhanced Permissions**: More sophisticated permission handling -- **Terminal Integration**: Full terminal support via opencode's bash tool - -## Testing - -```bash -# Run ACP tests -bun test test/acp.test.ts - -# Test manually with stdio -echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' | opencode acp -``` - -## Design Decisions - -### Why the Official Library? - -We use `@agentclientprotocol/sdk` instead of implementing JSON-RPC ourselves because: - -- Ensures protocol compliance -- Handles edge cases and future protocol versions -- Reduces maintenance burden -- Works with other ACP clients automatically - -### Clean Architecture - -Each component has a single responsibility: - -- **Agent** = Protocol interface -- **Client** = Client-side operations -- **Session** = State management -- **Server** = Lifecycle and I/O - -This makes the codebase maintainable and testable. - -### Mapping to OpenCode - -ACP sessions map cleanly to opencode's internal session model: - -- ACP `session/new` → creates internal Session -- ACP `session/prompt` → uses SessionPrompt.prompt() -- Working directory context preserved per-session -- Tool execution uses existing ToolRegistry - -## References - -- [ACP Specification](https://agentclientprotocol.com/) -- [TypeScript Library](https://github.com/agentclientprotocol/typescript-sdk) -- [Protocol Examples](https://github.com/agentclientprotocol/typescript-sdk/tree/main/src/examples) diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 1b804bec151..9c9f8d70b4a 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -3,2005 +3,93 @@ import { type Agent as ACPAgent, type AgentSideConnection, type AuthenticateRequest, - type AuthMethod, type CancelNotification, type CloseSessionRequest, - type CloseSessionResponse, type ForkSessionRequest, - type ForkSessionResponse, type InitializeRequest, - type InitializeResponse, type ListSessionsRequest, - type ListSessionsResponse, type LoadSessionRequest, type NewSessionRequest, - type PermissionOption, - type PlanEntry, type PromptRequest, type ResumeSessionRequest, - type ResumeSessionResponse, - type Role, - type SessionInfo, - type SetSessionModelRequest, - type SessionConfigOption, type SetSessionConfigOptionRequest, - type SetSessionConfigOptionResponse, + type SetSessionModelRequest, type SetSessionModeRequest, - type SetSessionModeResponse, - type ToolCallContent, - type ToolKind, - type Usage, } from "@agentclientprotocol/sdk" - -import * as Log from "@opencode-ai/core/util/log" -import { pathToFileURL } from "url" -import { Filesystem } from "@/util/filesystem" -import { Hash } from "@opencode-ai/core/util/hash" -import { ACPSessionManager } from "./session" -import type { ACPConfig } from "./types" -import { ACPRuntime } from "./runtime" -import { Provider } from "@/provider/provider" -import { ModelID, ProviderID } from "../provider/schema" -import { Installation } from "@/installation" -import { MessageV2 } from "@/session/message-v2" -import { Config } from "@/config/config" -import { ConfigMCP } from "@/config/mcp" -import { Todo } from "@/session/todo" -import { Result, Schema } from "effect" -import { LoadAPIKeyError } from "ai" -import type { - AssistantMessage, - Event, - GlobalEvent, - KiloClient, - SessionMessageResponse, - SyncEventMessagePartUpdated, - ToolPart, -} from "@kilocode/sdk/v2" -import { applyPatch } from "diff" -import { InstallationVersion } from "@opencode-ai/core/installation/version" - -import { fetchDefaultModel } from "@kilocode/kilo-gateway" // kilocode_change -import { ShellID } from "@/tool/shell/id" - -type ModeOption = { id: string; name: string; description?: string } -type ModelOption = { modelId: string; name: string } -type MessagePartUpdated = { - id: string - type: "message.part.updated" - properties: SyncEventMessagePartUpdated["data"] -} -type AgentEvent = Event | MessagePartUpdated -const decodeTodos = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Todo.Info))) - -const DEFAULT_VARIANT_VALUE = "default" - -const log = Log.create({ service: "acp-agent" }) - -async function getContextLimit( - sdk: KiloClient, - providerID: ProviderID, - modelID: ModelID, - directory: string, -): Promise { - const providers = await sdk.config - .providers({ directory }) - .then((x) => x.data?.providers ?? []) - .catch((error) => { - log.error("failed to get providers for context limit", { error }) - return [] - }) - - const provider = providers.find((p) => p.id === providerID) - const model = provider?.models[modelID] - return model?.limit.context ?? null -} - -async function sendUsageUpdate( - connection: AgentSideConnection, - sdk: KiloClient, - sessionID: string, - directory: string, -): Promise { - const messages = await sdk.session - .messages({ sessionID, directory }, { throwOnError: true }) - .then((x) => x.data) - .catch((error) => { - log.error("failed to fetch messages for usage update", { error }) - return undefined - }) - - if (!messages) return - - const assistantMessages = messages.filter( - (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant", - ) - - const lastAssistant = assistantMessages[assistantMessages.length - 1] - if (!lastAssistant) return - - const msg = lastAssistant.info - if (!msg.providerID || !msg.modelID) return - const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory) - - if (!size) { - // Cannot calculate usage without known context size - return - } - - const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0) - const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0) - - await connection - .sessionUpdate({ - sessionId: sessionID, - update: { - sessionUpdate: "usage_update", - used, - size, - cost: { amount: totalCost, currency: "USD" }, - }, - }) - .catch((error) => { - log.error("failed to send usage update", { error }) - }) -} +import { Effect } from "effect" +import type { KiloClient } from "@kilocode/sdk/v2" +import * as ACPError from "./error" +import * as ACPService from "./service" export function init({ sdk: _sdk }: { sdk: KiloClient }) { return { - create: (connection: AgentSideConnection, fullConfig: ACPConfig) => { - return new Agent(connection, fullConfig) + create: (connection: AgentSideConnection) => { + return new Agent(ACPService.make({ sdk: _sdk, connection })) }, } } export class Agent implements ACPAgent { - private connection: AgentSideConnection - private config: ACPConfig - private sdk: KiloClient - private sessionManager: ACPSessionManager - private eventAbort = new AbortController() - private eventStarted = false - private shellSnapshots = new Map() - private toolStarts = new Set() - private permissionQueues = new Map>() - private permissionOptions: PermissionOption[] = [ - { optionId: "once", kind: "allow_once", name: "Allow once" }, - { optionId: "always", kind: "allow_always", name: "Always allow" }, - { optionId: "reject", kind: "reject_once", name: "Reject" }, - ] + constructor(private readonly service: ACPService.Interface) {} - constructor(connection: AgentSideConnection, config: ACPConfig) { - this.connection = connection - this.config = config - this.sdk = config.sdk - this.sessionManager = new ACPSessionManager(this.sdk) - this.startEventSubscription() + initialize(params: InitializeRequest) { + return run(this.service.initialize(params)) } - private startEventSubscription() { - if (this.eventStarted) return - this.eventStarted = true - this.runEventSubscription().catch((error) => { - if (this.eventAbort.signal.aborted) return - log.error("event subscription failed", { error }) - }) + authenticate(params: AuthenticateRequest) { + return run(this.service.authenticate(params)) } - private async runEventSubscription() { - while (true) { - if (this.eventAbort.signal.aborted) return - const events = await this.sdk.global.event({ - signal: this.eventAbort.signal, - }) - for await (const event of events.stream) { - if (this.eventAbort.signal.aborted) return - const payload = event?.payload - if (!payload) continue - await this.handleEvent(payload).catch((error) => { - log.error("failed to handle event", { error, type: payload.type }) - }) - } - } + newSession(params: NewSessionRequest) { + return run(this.service.newSession(params)) } - private async handleEvent(payload: GlobalEvent["payload"]) { - const event: AgentEvent | undefined = (() => { - if (payload.type !== "sync") return payload - if (payload.name !== "message.part.updated.1") return undefined - return { id: payload.id, type: "message.part.updated", properties: payload.data } - })() - if (!event) return - - switch (event.type) { - case "permission.asked": { - const permission = event.properties - const session = this.sessionManager.tryGet(permission.sessionID) - if (!session) return - - const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve() - const next = prev - .then(async () => { - const directory = session.cwd - - const res = await this.connection - .requestPermission({ - sessionId: permission.sessionID, - toolCall: { - toolCallId: permission.tool?.callID ?? permission.id, - status: "pending", - title: permission.permission, - rawInput: permission.metadata, - kind: toToolKind(permission.permission), - locations: toLocations(permission.permission, permission.metadata), - }, - options: this.permissionOptions, - }) - .catch(async (error) => { - log.error("failed to request permission from ACP", { - error, - permissionID: permission.id, - sessionID: permission.sessionID, - }) - await this.sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - directory, - }) - return undefined - }) - - if (!res) return - if (res.outcome.outcome !== "selected") { - await this.sdk.permission.reply({ - requestID: permission.id, - reply: "reject", - directory, - }) - return - } - - if (res.outcome.optionId !== "reject" && permission.permission == "edit") { - const metadata = permission.metadata || {} - const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : "" - const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : "" - const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : "" - const newContent = getNewContent(content, diff) - - if (newContent) { - void this.connection.writeTextFile({ - sessionId: session.id, - path: filepath, - content: newContent, - }) - } - } - - await this.sdk.permission.reply({ - requestID: permission.id, - reply: res.outcome.optionId as "once" | "always" | "reject", - directory, - }) - }) - .catch((error) => { - log.error("failed to handle permission", { error, permissionID: permission.id }) - }) - .finally(() => { - if (this.permissionQueues.get(permission.sessionID) === next) { - this.permissionQueues.delete(permission.sessionID) - } - }) - this.permissionQueues.set(permission.sessionID, next) - return - } - - case "message.part.updated": { - log.info("message part updated", { event: event.properties }) - const props = event.properties - const part = props.part - const session = this.sessionManager.tryGet(part.sessionID) - if (!session) return - const sessionId = session.id - - if (part.type === "tool") { - await this.toolStart(sessionId, part) - - switch (part.state.status) { - case "pending": - this.shellSnapshots.delete(part.callID) - return - - case "running": - const output = this.shellOutput(part) - const content: ToolCallContent[] = [] - if (output) { - const hash = Hash.fast(output) - if (part.tool === ShellID.ToolID) { - if (this.shellSnapshots.get(part.callID) === hash) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - kind: toToolKind(part.tool), - title: part.tool, - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - }, - }) - .catch((error) => { - log.error("failed to send tool in_progress to ACP", { error }) - }) - return - } - this.shellSnapshots.set(part.callID, hash) - } - content.push({ - type: "content", - content: { - type: "text", - text: output, - }, - }) - } - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - kind: toToolKind(part.tool), - title: part.tool, - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - ...(content.length > 0 && { content }), - }, - }) - .catch((error) => { - log.error("failed to send tool in_progress to ACP", { error }) - }) - return - - case "completed": { - this.toolStarts.delete(part.callID) - this.shellSnapshots.delete(part.callID) - const kind = toToolKind(part.tool) - const content = completedToolContent(part, kind) - - if (part.tool === "todowrite") { - const parsedTodos = decodeTodos(part.state.output) - if (Result.isSuccess(parsedTodos)) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "plan", - entries: parsedTodos.success.map((todo) => { - const status: PlanEntry["status"] = - todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) - return { - priority: "medium", - status, - content: todo.content, - } - }), - }, - }) - .catch((error) => { - log.error("failed to send session update for todo", { error }) - }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.failure }) - } - } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "completed", - kind, - content, - title: part.state.title, - rawInput: part.state.input, - rawOutput: completedToolRawOutput(part), - }, - }) - .catch((error) => { - log.error("failed to send tool completed to ACP", { error }) - }) - return - } - case "error": - this.toolStarts.delete(part.callID) - this.shellSnapshots.delete(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "failed", - kind: toToolKind(part.tool), - title: part.tool, - rawInput: part.state.input, - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.error, - }, - }, - ], - rawOutput: { - error: part.state.error, - metadata: part.state.metadata, - }, - }, - }) - .catch((error) => { - log.error("failed to send tool error to ACP", { error }) - }) - return - } - } - - // ACP clients already know the prompt they just submitted, so replaying - // live user parts duplicates the message. We still replay user history in - // loadSession() and forkSession() via processMessage(). - if (part.type !== "text" && part.type !== "file") return - - return - } - - case "message.part.delta": { - const props = event.properties - const session = this.sessionManager.tryGet(props.sessionID) - if (!session) return - const sessionId = session.id - - const message = await this.sdk.session - .message( - { - sessionID: props.sessionID, - messageID: props.messageID, - directory: session.cwd, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((error) => { - log.error("unexpected error when fetching message", { error }) - return undefined - }) - - if (!message || message.info.role !== "assistant") return - - const part = message.parts.find((p) => p.id === props.partID) - if (!part) return - - if (part.type === "text" && props.field === "text" && part.ignored !== true) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_message_chunk", - messageId: props.messageID, - content: { - type: "text", - text: props.delta, - }, - }, - }) - .catch((error) => { - log.error("failed to send text delta to ACP", { error }) - }) - return - } - - if (part.type === "reasoning" && props.field === "text") { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - messageId: props.messageID, - content: { - type: "text", - text: props.delta, - }, - }, - }) - .catch((error) => { - log.error("failed to send reasoning delta to ACP", { error }) - }) - } - return - } - } + loadSession(params: LoadSessionRequest) { + return run(this.service.loadSession(params)) } - async initialize(params: InitializeRequest): Promise { - log.info("initialize", { protocolVersion: params.protocolVersion }) - - // kilocode_change start - const authMethod: AuthMethod = { - description: "Run `kilo auth login` in the terminal", - name: "Login with Kilo", - id: "kilo-login", - } - // kilocode_change end - - // If client supports terminal-auth capability, use that instead. - if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { - authMethod._meta = { - "terminal-auth": { - command: "opencode", - args: ["auth", "login"], - label: "Kilo Login", - }, - } - } - - return { - protocolVersion: 1, - agentCapabilities: { - loadSession: true, - mcpCapabilities: { - http: true, - sse: true, - }, - promptCapabilities: { - embeddedContext: true, - image: true, - }, - sessionCapabilities: { - close: {}, - fork: {}, - list: {}, - resume: {}, - }, - }, - authMethods: [authMethod], - agentInfo: { - name: "Kilo", - version: InstallationVersion, - }, - } + listSessions(params: ListSessionsRequest) { + return run(this.service.listSessions(params)) } - async authenticate(_params: AuthenticateRequest) { - throw new Error("Authentication not implemented") + resumeSession(params: ResumeSessionRequest) { + return run(this.service.resumeSession(params)) } - async newSession(params: NewSessionRequest) { - const directory = params.cwd - try { - const model = await defaultModel(this.config, directory) - - // Store ACP session state - const state = await this.sessionManager.create(params.cwd, params.mcpServers, model) - const sessionId = state.id - - log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length }) - - const load = await this.loadSessionMode({ - cwd: directory, - mcpServers: params.mcpServers, - sessionId, - }) - - return { - sessionId, - configOptions: load.configOptions, - models: load.models, - modes: load.modes, - _meta: load._meta, - } - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } + closeSession(params: CloseSessionRequest) { + return run(this.service.closeSession(params)) } - async loadSession(params: LoadSessionRequest) { - const directory = params.cwd - const sessionId = params.sessionId - - try { - const model = await defaultModel(this.config, directory) - - // Store ACP session state - await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) - - const messages = await this.loadSessionMessages(directory, sessionId) - this.restoreSessionStateFromMessages(sessionId, messages) - - log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) - - const result = await this.loadSessionMode({ - cwd: directory, - mcpServers: params.mcpServers, - sessionId, - }) - - for (const msg of messages ?? []) { - log.debug("replay message", msg) - await this.processMessage(msg) - } - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return result - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } + unstable_forkSession(params: ForkSessionRequest) { + return run(this.service.forkSession(params)) } - async listSessions(params: ListSessionsRequest): Promise { - try { - const cursor = params.cursor ? Number(params.cursor) : undefined - const limit = 100 - - const sessions = await this.sdk.session - .list( - { - directory: params.cwd ?? undefined, - roots: true, - }, - { throwOnError: true }, - ) - .then((x) => x.data ?? []) - - const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated) - const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted - const page = filtered.slice(0, limit) - - const entries: SessionInfo[] = page.map((session) => ({ - sessionId: session.id, - cwd: session.directory, - title: session.title, - updatedAt: new Date(session.time.updated).toISOString(), - })) - - const last = page[page.length - 1] - const next = filtered.length > limit && last ? String(last.time.updated) : undefined - - const response: ListSessionsResponse = { - sessions: entries, - } - if (next) response.nextCursor = next - return response - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } + setSessionConfigOption(params: SetSessionConfigOptionRequest) { + return run(this.service.setSessionConfigOption(params)) } - async unstable_forkSession(params: ForkSessionRequest): Promise { - const directory = params.cwd - const mcpServers = params.mcpServers ?? [] - - try { - const model = await defaultModel(this.config, directory) - - const forked = await this.sdk.session - .fork( - { - sessionID: params.sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - - if (!forked) { - throw new Error("Fork session returned no data") - } - - const sessionId = forked.id - await this.sessionManager.load(sessionId, directory, mcpServers, model) - - const messages = await this.loadSessionMessages(directory, sessionId) - this.restoreSessionStateFromMessages(sessionId, messages) - - log.info("fork_session", { sessionId, mcpServers: mcpServers.length }) - - const mode = await this.loadSessionMode({ - cwd: directory, - mcpServers, - sessionId, - }) - - for (const msg of messages ?? []) { - log.debug("replay message", msg) - await this.processMessage(msg) - } - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return mode - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } + setSessionMode(params: SetSessionModeRequest) { + return run(this.service.setSessionMode(params)) } - async resumeSession(params: ResumeSessionRequest): Promise { - const directory = params.cwd - const sessionId = params.sessionId - const mcpServers = params.mcpServers ?? [] - - try { - const model = await defaultModel(this.config, directory) - await this.sessionManager.load(sessionId, directory, mcpServers, model) - - const messages = await this.loadSessionMessages(directory, sessionId, 20) - this.restoreSessionStateFromMessages(sessionId, messages) - - log.info("resume_session", { sessionId, mcpServers: mcpServers.length }) - - const result = await this.loadSessionMode({ - cwd: directory, - mcpServers, - sessionId, - }) - - await sendUsageUpdate(this.connection, this.sdk, sessionId, directory) - - return result - } catch (e) { - const error = MessageV2.fromError(e, { - providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"), - }) - if (LoadAPIKeyError.isInstance(error)) { - throw RequestError.authRequired() - } - throw e - } + unstable_setSessionModel(params: SetSessionModelRequest) { + return run(this.service.setSessionModel(params)) } - async closeSession(params: CloseSessionRequest): Promise { - const session = this.sessionManager.remove(params.sessionId) - if (!session) return {} - - await this.sdk.session - .abort( - { - sessionID: params.sessionId, - directory: session.cwd, - }, - { throwOnError: true }, - ) - .catch((error) => { - log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId }) - }) - - this.permissionQueues.delete(params.sessionId) - log.info("close_session", { sessionId: params.sessionId }) - return {} + prompt(params: PromptRequest) { + return run(this.service.prompt(params)) } - private async processMessage(message: SessionMessageResponse) { - log.debug("process message", message) - if (message.info.role !== "assistant" && message.info.role !== "user") return - const sessionId = message.info.sessionID - - for (const part of message.parts) { - if (part.type === "tool") { - await this.toolStart(sessionId, part) - switch (part.state.status) { - case "pending": - this.shellSnapshots.delete(part.callID) - break - case "running": - const output = this.shellOutput(part) - const runningContent: ToolCallContent[] = [] - if (output) { - runningContent.push({ - type: "content", - content: { - type: "text", - text: output, - }, - }) - } - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "in_progress", - kind: toToolKind(part.tool), - title: part.tool, - locations: toLocations(part.tool, part.state.input), - rawInput: part.state.input, - ...(runningContent.length > 0 && { content: runningContent }), - }, - }) - .catch((err) => { - log.error("failed to send tool in_progress to ACP", { error: err }) - }) - break - case "completed": - this.toolStarts.delete(part.callID) - this.shellSnapshots.delete(part.callID) - const kind = toToolKind(part.tool) - const content = completedToolContent(part, kind) - - if (part.tool === "todowrite") { - const parsedTodos = decodeTodos(part.state.output) - if (Result.isSuccess(parsedTodos)) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "plan", - entries: parsedTodos.success.map((todo) => { - const status: PlanEntry["status"] = - todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) - return { - priority: "medium", - status, - content: todo.content, - } - }), - }, - }) - .catch((err) => { - log.error("failed to send session update for todo", { error: err }) - }) - } else { - log.error("failed to parse todo output", { error: parsedTodos.failure }) - } - } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "completed", - kind, - content, - title: part.state.title, - rawInput: part.state.input, - rawOutput: completedToolRawOutput(part), - }, - }) - .catch((err) => { - log.error("failed to send tool completed to ACP", { error: err }) - }) - break - case "error": - this.toolStarts.delete(part.callID) - this.shellSnapshots.delete(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call_update", - toolCallId: part.callID, - status: "failed", - kind: toToolKind(part.tool), - title: part.tool, - rawInput: part.state.input, - content: [ - { - type: "content", - content: { - type: "text", - text: part.state.error, - }, - }, - ], - rawOutput: { - error: part.state.error, - metadata: part.state.metadata, - }, - }, - }) - .catch((err) => { - log.error("failed to send tool error to ACP", { error: err }) - }) - break - } - } else if (part.type === "text") { - if (part.text) { - const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk", - messageId: message.info.id, - content: { - type: "text", - text: part.text, - ...(audience && { annotations: { audience } }), - }, - }, - }) - .catch((err) => { - log.error("failed to send text to ACP", { error: err }) - }) - } - } else if (part.type === "file") { - // Replay file attachments as appropriate ACP content blocks. - // OpenCode stores files internally as { type: "file", url, filename, mime }. - // We convert these back to ACP blocks based on the URL scheme and MIME type: - // - file:// URLs → resource_link - // - data: URLs with image/* → image block - // - data: URLs with text/* or application/json → resource with text - // - data: URLs with other types → resource with blob - const url = part.url - const filename = part.filename ?? "file" - const mime = part.mime || "application/octet-stream" - const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk" - - if (url.startsWith("file://")) { - // Local file reference - send as resource_link - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: messageChunk, - messageId: message.info.id, - content: { type: "resource_link", uri: url, name: filename, mimeType: mime }, - }, - }) - .catch((err) => { - log.error("failed to send resource_link to ACP", { error: err }) - }) - } else if (url.startsWith("data:")) { - // Embedded content - parse data URL and send as appropriate block type - const base64Match = url.match(/^data:([^;]+);base64,(.*)$/) - const dataMime = base64Match?.[1] - const base64Data = base64Match?.[2] ?? "" - - const effectiveMime = dataMime || mime - - if (effectiveMime.startsWith("image/")) { - // Image - send as image block - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: messageChunk, - messageId: message.info.id, - content: { - type: "image", - mimeType: effectiveMime, - data: base64Data, - uri: pathToFileURL(filename).href, - }, - }, - }) - .catch((err) => { - log.error("failed to send image to ACP", { error: err }) - }) - } else { - // Non-image: text types get decoded, binary types stay as blob - const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json" - const fileUri = pathToFileURL(filename).href - const resource = isText - ? { - uri: fileUri, - mimeType: effectiveMime, - text: Buffer.from(base64Data, "base64").toString("utf-8"), - } - : { uri: fileUri, mimeType: effectiveMime, blob: base64Data } - - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: messageChunk, - messageId: message.info.id, - content: { type: "resource", resource }, - }, - }) - .catch((err) => { - log.error("failed to send resource to ACP", { error: err }) - }) - } - } - // URLs that don't match file:// or data: are skipped (unsupported) - } else if (part.type === "reasoning") { - if (part.text) { - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "agent_thought_chunk", - messageId: message.info.id, - content: { - type: "text", - text: part.text, - }, - }, - }) - .catch((err) => { - log.error("failed to send reasoning to ACP", { error: err }) - }) - } - } - } - } - - private shellOutput(part: ToolPart) { - if (part.tool !== ShellID.ToolID) return - if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return - const output = part.state.metadata["output"] - if (typeof output !== "string") return - return output - } - - private async toolStart(sessionId: string, part: ToolPart) { - if (this.toolStarts.has(part.callID)) return - this.toolStarts.add(part.callID) - await this.connection - .sessionUpdate({ - sessionId, - update: { - sessionUpdate: "tool_call", - toolCallId: part.callID, - title: part.tool, - kind: toToolKind(part.tool), - status: "pending", - locations: [], - rawInput: {}, - }, - }) - .catch((error) => { - log.error("failed to send tool pending to ACP", { error }) - }) - } - - private async loadAvailableModes(directory: string): Promise { - const agents = await this.config.sdk.app - .agents( - { - directory, - }, - { throwOnError: true }, - ) - .then((resp) => resp.data!) - - return agents - .filter((agent) => agent.mode !== "subagent" && !agent.hidden) - .map((agent) => ({ - id: agent.name, - name: agent.name, - description: agent.description, - })) - } - - private async resolveModeState( - directory: string, - sessionId: string, - ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { - const availableModes = await this.loadAvailableModes(directory) - const storedModeId = this.sessionManager.get(sessionId).modeId - if (storedModeId && availableModes.some((mode) => mode.id === storedModeId)) { - return { availableModes, currentModeId: storedModeId } - } - - const currentModeId = await (async () => { - if (!availableModes.length) return undefined - const defaultAgent = await ACPRuntime.defaultAgentInfo(directory) - const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgent.name)?.id ?? availableModes[0].id - this.sessionManager.setMode(sessionId, resolvedModeId) - return resolvedModeId - })() - - return { availableModes, currentModeId } - } - - private async loadSessionMode(params: LoadSessionRequest) { - const directory = params.cwd - const sessionId = params.sessionId - const model = this.sessionManager.get(sessionId).model ?? (await defaultModel(this.config, directory)) - - const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, model) - const currentVariant = this.sessionManager.getVariant(sessionId) - if (currentVariant && !availableVariants.includes(currentVariant)) { - this.sessionManager.setVariant(sessionId, undefined) - } - const availableModels = buildAvailableModels(entries) - const modeState = await this.resolveModeState(directory, sessionId) - const currentModeId = modeState.currentModeId - const modes = currentModeId - ? { - availableModes: modeState.availableModes, - currentModeId, - } - : undefined - - const commands = await this.config.sdk.command - .list( - { - directory, - }, - { throwOnError: true }, - ) - .then((resp) => resp.data!) - - const availableCommands = commands.map((command) => ({ - name: command.name, - description: command.description ?? "", - })) - const names = new Set(availableCommands.map((c) => c.name)) - if (!names.has("compact")) - availableCommands.push({ - name: "compact", - description: "compact the session", - }) - - const mcpServers: Record = {} - for (const server of params.mcpServers) { - if ("type" in server) { - mcpServers[server.name] = { - url: server.url, - headers: server.headers.reduce>((acc, { name, value }) => { - acc[name] = value - return acc - }, {}), - type: "remote", - } - } else { - mcpServers[server.name] = { - type: "local", - command: [server.command, ...server.args], - environment: server.env.reduce>((acc, { name, value }) => { - acc[name] = value - return acc - }, {}), - } - } - } - - await Promise.all( - Object.entries(mcpServers).map(async ([key, mcp]) => { - await this.sdk.mcp - .add( - { - directory, - name: key, - config: mcp, - }, - { throwOnError: true }, - ) - .catch((error) => { - log.error("failed to add mcp server", { name: key, error }) - }) - }), - ) - - setTimeout(() => { - void this.connection.sessionUpdate({ - sessionId, - update: { - sessionUpdate: "available_commands_update", - availableCommands, - }, - }) - }, 0) - - return { - sessionId, - models: { - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false), - availableModels, - }, - modes, - configOptions: buildConfigOptions({ - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false), - availableModels, - currentVariant, - availableVariants, - modes, - }), - _meta: buildVariantMeta({ - model, - variant: this.sessionManager.getVariant(sessionId), - availableVariants, - }), - } - } - - async unstable_setSessionModel(params: SetSessionModelRequest) { - const session = this.sessionManager.get(params.sessionId) - const providers = await this.sdk.config - .providers({ directory: session.cwd }, { throwOnError: true }) - .then((x) => x.data!.providers) - - const selection = parseModelSelection(params.modelId, providers) - this.sessionManager.setModel(session.id, selection.model) - this.sessionManager.setVariant(session.id, selection.variant) - - const entries = sortProvidersByName(providers) - const availableVariants = modelVariantsFromProviders(entries, selection.model) - const modeState = await this.resolveModeState(session.cwd, session.id) - const modes = modeState.currentModeId - ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } - : undefined - - await this.connection.sessionUpdate({ - sessionId: session.id, - update: { - sessionUpdate: "config_option_update", - configOptions: buildConfigOptions({ - currentModelId: formatModelIdWithVariant(selection.model, selection.variant, availableVariants, false), - availableModels: buildAvailableModels(entries), - currentVariant: selection.variant, - availableVariants, - modes, - }), - }, - }) - - return { - _meta: buildVariantMeta({ - model: selection.model, - variant: selection.variant, - availableVariants, - }), - } - } - - async setSessionMode(params: SetSessionModeRequest): Promise { - const session = this.sessionManager.get(params.sessionId) - const availableModes = await this.loadAvailableModes(session.cwd) - if (!availableModes.some((mode) => mode.id === params.modeId)) { - throw new Error(`Agent not found: ${params.modeId}`) - } - this.sessionManager.setMode(params.sessionId, params.modeId) - } - - async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise { - const session = this.sessionManager.get(params.sessionId) - const providers = await this.sdk.config - .providers({ directory: session.cwd }, { throwOnError: true }) - .then((x) => x.data!.providers) - const entries = sortProvidersByName(providers) - - if (params.configId === "model") { - if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string") - const selection = parseModelSelection(params.value, providers) - this.sessionManager.setModel(session.id, selection.model) - this.sessionManager.setVariant(session.id, selection.variant) - } else if (params.configId === "effort") { - if (typeof params.value !== "string") throw RequestError.invalidParams("effort value must be a string") - const current = session.model ?? (await defaultModel(this.config, session.cwd)) - const availableVariants = modelVariantsFromProviders(entries, current) - if (!availableVariants.includes(params.value)) { - throw RequestError.invalidParams(JSON.stringify({ error: `Effort not found: ${params.value}` })) - } - this.sessionManager.setVariant(session.id, params.value) - } else if (params.configId === "mode") { - if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string") - const availableModes = await this.loadAvailableModes(session.cwd) - if (!availableModes.some((mode) => mode.id === params.value)) { - throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` })) - } - this.sessionManager.setMode(session.id, params.value) - } else { - throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` })) - } - - const updatedSession = this.sessionManager.get(session.id) - const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd)) - const availableVariants = modelVariantsFromProviders(entries, model) - const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, false) - const availableModels = buildAvailableModels(entries) - const modeState = await this.resolveModeState(session.cwd, session.id) - const modes = modeState.currentModeId - ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } - : undefined - - return { - configOptions: buildConfigOptions({ - currentModelId, - availableModels, - currentVariant: updatedSession.variant, - availableVariants, - modes, - }), - } - } - - async prompt(params: PromptRequest) { - const sessionID = params.sessionId - const session = this.sessionManager.get(sessionID) - const directory = session.cwd - - const current = session.model - const model = current ?? (await defaultModel(this.config, directory)) - if (!current) { - this.sessionManager.setModel(session.id, model) - } - const agent = session.modeId ?? (await ACPRuntime.defaultAgentInfo(directory)).name - - const parts: Array< - | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean } - | { type: "file"; url: string; filename: string; mime: string } - > = [] - for (const part of params.prompt) { - switch (part.type) { - case "text": - const audience = part.annotations?.audience - const forAssistant = audience?.length === 1 && audience[0] === "assistant" - const forUser = audience?.length === 1 && audience[0] === "user" - parts.push({ - type: "text" as const, - text: part.text, - ...(forAssistant && { synthetic: true }), - ...(forUser && { ignored: true }), - }) - break - case "image": { - const parsed = parseUri(part.uri ?? "") - const filename = parsed.type === "file" ? parsed.filename : "image" - if (part.data) { - parts.push({ - type: "file", - url: `data:${part.mimeType};base64,${part.data}`, - filename, - mime: part.mimeType, - }) - } else if (part.uri && part.uri.startsWith("http:")) { - parts.push({ - type: "file", - url: part.uri, - filename, - mime: part.mimeType, - }) - } - break - } - - case "resource_link": - const parsed = parseUri(part.uri) - // Use the name from resource_link if available - if (part.name && parsed.type === "file") { - parsed.filename = part.name - } - parts.push(parsed) - - break - - case "resource": { - const resource = part.resource - if ("text" in resource && resource.text) { - parts.push({ - type: "text", - text: resource.text, - }) - } else if ("blob" in resource && resource.blob && resource.mimeType) { - // Binary resource (PDFs, etc.): store as file part with data URL - const parsed = parseUri(resource.uri ?? "") - const filename = parsed.type === "file" ? parsed.filename : "file" - parts.push({ - type: "file", - url: `data:${resource.mimeType};base64,${resource.blob}`, - filename, - mime: resource.mimeType, - }) - } - break - } - - default: - break - } - } - - log.info("parts", { parts }) - - const cmd = (() => { - const text = parts - .filter((p): p is { type: "text"; text: string } => p.type === "text") - .map((p) => p.text) - .join("") - .trim() - - if (!text.startsWith("/")) return - - const [name, ...rest] = text.slice(1).split(/\s+/) - return { name, args: rest.join(" ").trim() } - })() - - const buildUsage = (msg: AssistantMessage): Usage => ({ - totalTokens: - msg.tokens.input + - msg.tokens.output + - msg.tokens.reasoning + - (msg.tokens.cache?.read ?? 0) + - (msg.tokens.cache?.write ?? 0), - inputTokens: msg.tokens.input, - outputTokens: msg.tokens.output, - thoughtTokens: msg.tokens.reasoning || undefined, - cachedReadTokens: msg.tokens.cache?.read || undefined, - cachedWriteTokens: msg.tokens.cache?.write || undefined, - }) - - if (!cmd) { - const response = await this.sdk.session.prompt({ - sessionID, - model: { - providerID: model.providerID, - modelID: model.modelID, - }, - variant: this.sessionManager.getVariant(sessionID), - parts, - agent, - directory, - }) - const msg = response.data?.info - - await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - - return { - stopReason: "end_turn" as const, - usage: msg ? buildUsage(msg) : undefined, - _meta: {}, - } - } - - const command = await this.config.sdk.command - .list({ directory }, { throwOnError: true }) - .then((x) => x.data!.find((c) => c.name === cmd.name)) - if (command) { - const response = await this.sdk.session.command({ - sessionID, - command: command.name, - arguments: cmd.args, - model: model.providerID + "/" + model.modelID, - agent, - directory, - }) - const msg = response.data?.info - - await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - - return { - stopReason: "end_turn" as const, - usage: msg ? buildUsage(msg) : undefined, - _meta: {}, - } - } - - switch (cmd.name) { - case "compact": - await this.config.sdk.session.summarize( - { - sessionID, - directory, - providerID: model.providerID, - modelID: model.modelID, - }, - { throwOnError: true }, - ) - break - } - - await sendUsageUpdate(this.connection, this.sdk, sessionID, directory) - - return { - stopReason: "end_turn" as const, - _meta: {}, - } - } - - async cancel(params: CancelNotification) { - const session = this.sessionManager.get(params.sessionId) - await this.config.sdk.session.abort( - { - sessionID: params.sessionId, - directory: session.cwd, - }, - { throwOnError: true }, - ) - } - - private async loadSessionMessages(directory: string, sessionId: string, limit?: number) { - return this.sdk.session - .messages( - { - sessionID: sessionId, - directory, - limit, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((error) => { - log.error("unexpected error when fetching message", { error }) - return undefined - }) - } - - private restoreSessionStateFromMessages(sessionId: string, messages: SessionMessageResponse[] | undefined) { - const lastUser = messages?.findLast((message) => message.info.role === "user")?.info - if (lastUser?.role !== "user") return - - this.sessionManager.setModel(sessionId, { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), - }) - this.sessionManager.setVariant(sessionId, lastUser.model.variant) - if (lastUser.agent) { - this.sessionManager.setMode(sessionId, lastUser.agent) - } + cancel(params: CancelNotification) { + return run(this.service.cancel(params)) } } -function toToolKind(toolName: string): ToolKind { - const tool = toolName.toLocaleLowerCase() - - switch (tool) { - case ShellID.ToolID: - return "execute" - - case "webfetch": - return "fetch" - - case "edit": - case "patch": - case "write": - return "edit" - - case "grep": - case "glob": - case "repo_clone": - case "repo_overview": - case "context7_resolve_library_id": - case "context7_get_library_docs": - return "search" - - case "read": - return "read" - - default: - return "other" - } -} - -function toLocations(toolName: string, input: Record): { path: string }[] { - const tool = toolName.toLocaleLowerCase() - - switch (tool) { - case "read": - case "edit": - case "write": - return input["filePath"] ? [{ path: input["filePath"] }] : [] - case "glob": - case "grep": - return input["path"] ? [{ path: input["path"] }] : [] - case "repo_clone": - return input["path"] ? [{ path: input["path"] }] : [] - case "repo_overview": - return input["path"] ? [{ path: input["path"] }] : [] - case ShellID.ToolID: - return [] - default: - return [] - } -} - -function completedToolContent(part: ToolPart, kind: ToolKind): ToolCallContent[] { - if (part.state.status !== "completed") return [] - - const content: ToolCallContent[] = [ - { - type: "content", - content: { - type: "text", - text: part.state.output, - }, - }, - ] - - if (kind === "edit") { - const input = part.state.input - const filePath = typeof input["filePath"] === "string" ? input["filePath"] : "" - const oldText = typeof input["oldString"] === "string" ? input["oldString"] : "" - const newText = - typeof input["newString"] === "string" - ? input["newString"] - : typeof input["content"] === "string" - ? input["content"] - : "" - content.push({ - type: "diff", - path: filePath, - oldText, - newText, - }) - } - - content.push(...imageContents(part.state.attachments ?? [])) - return content -} - -function completedToolRawOutput(part: ToolPart) { - if (part.state.status !== "completed") return {} - return { - output: part.state.output, - metadata: part.state.metadata, - ...(part.state.attachments?.length ? { attachments: part.state.attachments } : {}), - } -} - -function imageContents(attachments: Array<{ mime: string; url: string }>): ToolCallContent[] { - return attachments.flatMap((attachment): ToolCallContent[] => { - const match = attachment.url.match(/^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/) - const mime = match?.[1] ?? attachment.mime - if (!mime.startsWith("image/")) return [] - const data = match?.[2] - if (data === undefined) return [] - return [ - { - type: "content" as const, - content: { - type: "image" as const, - mimeType: mime, - data, - }, - }, - ] +function run(effect: Effect.Effect) { + return Effect.runPromise(effect.pipe(Effect.mapError(ACPError.toRequestError))).catch((defect: unknown) => { + if (defect instanceof RequestError) throw defect + throw ACPError.toRequestError(ACPError.fromUnknownDefect(defect)) }) } -async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> { - const sdk = config.sdk - const configured = config.defaultModel - if (configured) return configured - - const directory = cwd ?? process.cwd() - - const specified = await sdk.config - .get({ directory }, { throwOnError: true }) - .then((resp) => { - const cfg = resp.data - if (!cfg || !cfg.model) return undefined - return Provider.parseModel(cfg.model) - }) - .catch((error) => { - log.error("failed to load user config for default model", { error }) - return undefined - }) - - const providers = await sdk.config - .providers({ directory }, { throwOnError: true }) - .then((x) => x.data?.providers ?? []) - .catch((error) => { - log.error("failed to list providers for default model", { error }) - return [] - }) - - if (specified && providers.length) { - const provider = providers.find((p) => p.id === specified.providerID) - if (provider && provider.models[specified.modelID]) return specified - } - - if (specified && !providers.length) return specified - - // kilocode_change start - const lastUsed = await lastUsedModel(sdk, directory, providers) - if (lastUsed) return lastUsed - - const kiloProvider = providers.find((p) => p.id === "kilo") - if (kiloProvider) { - const [best] = Provider.sort(Object.values(kiloProvider.models)) - if (best) { - return { - providerID: ProviderID.make(best.providerID), - modelID: ModelID.make(best.id), - } - } - } - // kilocode_change end - - const models = providers.flatMap((p) => Object.values(p.models)) - const [best] = Provider.sort(models) - if (best) { - return { - providerID: ProviderID.make(best.providerID), - modelID: ModelID.make(best.id), - } - } - - if (specified) return specified - - // kilocode_change start - prefer kilo free-tier default before erroring. - // Only fall back to the Kilo provider if it was present in the available - // providers list. When teams configure enabled_providers to use only their - // own models, this prevents silently routing requests to an external API. - // Note: LiteLLM / custom provider users won't reach here — the function - // returns earlier via `specified` (config.model) or the sorted providers list. - if (providers.some((p) => p.id === "kilo")) { - const freeModel = await fetchDefaultModel() - return { providerID: ProviderID.kilo, modelID: ModelID.make(freeModel) } - } - // kilocode_change end - throw new Error("No models available") -} - -async function lastUsedModel( - sdk: KiloClient, - directory: string, - providers: Array<{ id: string; models: Record }>, -): Promise<{ providerID: ProviderID; modelID: ModelID } | undefined> { - const session = await sdk.session - .list({ directory, roots: true, limit: 1 }, { throwOnError: true }) - .then((x) => x.data?.[0]) - .catch((error) => { - log.error("failed to list sessions for default model", { error }) - return undefined - }) - if (!session) return - - const lastUser = await sdk.session - .messages({ sessionID: session.id, directory, limit: 20 }, { throwOnError: true }) - .then((x) => x.data?.findLast((message) => message.info.role === "user")?.info) - .catch((error) => { - log.error("failed to load session messages for default model", { error, sessionID: session.id }) - return undefined - }) - if (lastUser?.role !== "user") return - - const provider = providers.find((entry) => entry.id === lastUser.model.providerID) - if (!provider?.models[lastUser.model.modelID]) return - return { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), - } -} - -function parseUri( - uri: string, -): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } { - try { - if (uri.startsWith("file://")) { - const path = uri.slice(7) - const name = path.split("/").pop() || path - return { - type: "file", - url: uri, - filename: name, - mime: "text/plain", - } - } - if (uri.startsWith("zed://")) { - const url = new URL(uri) - const path = url.searchParams.get("path") - if (path) { - const name = path.split("/").pop() || path - return { - type: "file", - url: pathToFileURL(path).href, - filename: name, - mime: "text/plain", - } - } - } - return { - type: "text", - text: uri, - } - } catch { - return { - type: "text", - text: uri, - } - } -} - -function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined { - const result = applyPatch(fileOriginal, unifiedDiff) - if (result === false) { - log.error("Failed to apply unified diff (context mismatch)") - return undefined - } - return result -} - -function sortProvidersByName(providers: T[]): T[] { - return [...providers].sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) return -1 - if (nameA > nameB) return 1 - return 0 - }) -} - -function modelVariantsFromProviders( - providers: Array<{ id: string; models: Record }> }>, - model: { providerID: ProviderID; modelID: ModelID }, -): string[] { - const provider = providers.find((entry) => entry.id === model.providerID) - if (!provider) return [] - const modelInfo = provider.models[model.modelID] - if (!modelInfo?.variants) return [] - return Object.keys(modelInfo.variants) -} - -function buildAvailableModels( - providers: Array<{ id: string; name: string; models: Record }>, - options: { includeVariants?: boolean } = {}, -): ModelOption[] { - const includeVariants = options.includeVariants ?? false - return providers.flatMap((provider) => { - const unsorted: Array<{ id: string; name: string; variants?: Record }> = Object.values(provider.models) - const models = Provider.sort(unsorted) - return models.flatMap((model) => { - const base: ModelOption = { - modelId: `${provider.id}/${model.id}`, - name: `${provider.name}/${model.name}`, - } - if (!includeVariants || !model.variants) return [base] - const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE) - const variantOptions = variants.map((variant) => ({ - modelId: `${provider.id}/${model.id}/${variant}`, - name: `${provider.name}/${model.name} (${variant})`, - })) - return [base, ...variantOptions] - }) - }) -} - -function formatModelIdWithVariant( - model: { providerID: ProviderID; modelID: ModelID }, - variant: string | undefined, - availableVariants: string[], - includeVariant: boolean, -) { - const base = `${model.providerID}/${model.modelID}` - if (!includeVariant || availableVariants.length === 0) return base - const selectedVariant = - variant && availableVariants.includes(variant) - ? variant - : availableVariants.includes(DEFAULT_VARIANT_VALUE) - ? DEFAULT_VARIANT_VALUE - : availableVariants[0] - return `${base}/${selectedVariant}` -} - -function buildVariantMeta(input: { - model: { providerID: ProviderID; modelID: ModelID } - variant?: string - availableVariants: string[] -}) { - return { - opencode: { - modelId: `${input.model.providerID}/${input.model.modelID}`, - variant: input.variant ?? null, - availableVariants: input.availableVariants, - }, - } -} - -function parseModelSelection( - modelId: string, - providers: Array<{ id: string; models: Record }> }>, -): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } { - const parsed = Provider.parseModel(modelId) - const provider = providers.find((p) => p.id === parsed.providerID) - if (!provider) { - return { model: parsed, variant: undefined } - } - - // Check if modelID exists directly - if (provider.models[parsed.modelID]) { - return { model: parsed, variant: undefined } - } - - // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high") - const segments = parsed.modelID.split("/") - if (segments.length > 1) { - const candidateVariant = segments[segments.length - 1] - const baseModelId = segments.slice(0, -1).join("/") - const baseModelInfo = provider.models[baseModelId] - if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) { - return { - model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) }, - variant: candidateVariant, - } - } - } - - return { model: parsed, variant: undefined } -} - -function buildConfigOptions(input: { - currentModelId: string - availableModels: ModelOption[] - currentVariant?: string - availableVariants?: string[] - modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined -}): SessionConfigOption[] { - const options: SessionConfigOption[] = [ - { - id: "model", - name: "Model", - category: "model", - type: "select", - currentValue: input.currentModelId, - options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })), - }, - ] - if (input.availableVariants?.length) { - options.push({ - id: "effort", - name: "Effort", - description: "Available effort levels for this model", - category: "thought_level", - type: "select", - currentValue: - input.currentVariant && input.availableVariants.includes(input.currentVariant) - ? input.currentVariant - : input.availableVariants.includes(DEFAULT_VARIANT_VALUE) - ? DEFAULT_VARIANT_VALUE - : input.availableVariants[0], - options: input.availableVariants.map((variant) => ({ value: variant, name: formatVariantName(variant) })), - }) - } - if (input.modes) { - options.push({ - id: "mode", - name: "Session Mode", - category: "mode", - type: "select", - currentValue: input.modes.currentModeId, - options: input.modes.availableModes.map((m) => ({ - value: m.id, - name: m.name, - ...(m.description ? { description: m.description } : {}), - })), - }) - } - return options -} - -function formatVariantName(variant: string) { - return variant - .split(/[_-]/) - .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part)) - .join(" ") -} - export * as ACP from "./agent" diff --git a/packages/opencode/src/acp/config-option.ts b/packages/opencode/src/acp/config-option.ts new file mode 100644 index 00000000000..b730ae07533 --- /dev/null +++ b/packages/opencode/src/acp/config-option.ts @@ -0,0 +1,203 @@ +import type { SessionConfigOption } from "@agentclientprotocol/sdk" + +export const DEFAULT_VARIANT_VALUE = "default" + +export type ConfigOptionModel = { + id: string + name: string + variants?: Record> +} + +export type ConfigOptionProvider = { + id: string + name: string + models: Record +} + +export type ConfigOptionMode = { + id: string + name: string + description?: string +} + +export type ModelSelection = { + model: { + providerID: string + modelID: string + } + variant?: string +} + +export function buildModelSelectOption(input: { + providers: readonly ConfigOptionProvider[] + currentModel: ModelSelection["model"] + currentVariant?: string + includeVariants?: boolean +}): SessionConfigOption { + return { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: formatCurrentModelId({ + model: input.currentModel, + variant: input.currentVariant, + variants: variantsForModel(input.providers, input.currentModel), + includeVariant: input.includeVariants ?? false, + }), + options: buildModelSelectOptions(input.providers, { includeVariants: input.includeVariants ?? false }), + } +} + +export function buildEffortSelectOption(input: { + variants: readonly string[] + currentVariant?: string +}): SessionConfigOption | undefined { + if (input.variants.length === 0) return undefined + + return { + id: "effort", + name: "Effort", + description: "Available effort levels for this model", + category: "thought_level", + type: "select", + currentValue: selectVariant(input.currentVariant, input.variants), + options: input.variants.map((variant) => ({ + value: variant, + name: formatVariantName(variant), + })), + } +} + +export function buildModeSelectOption(input: { + modes: readonly ConfigOptionMode[] + currentModeId: string +}): SessionConfigOption { + return { + id: "mode", + name: "Session Mode", + category: "mode", + type: "select", + currentValue: input.currentModeId, + options: input.modes.map((mode) => ({ + value: mode.id, + name: mode.name, + ...(mode.description ? { description: mode.description } : {}), + })), + } +} + +export function buildConfigOptions(input: { + providers: readonly ConfigOptionProvider[] + currentModel: ModelSelection["model"] + currentVariant?: string + includeModelVariants?: boolean + modes?: readonly ConfigOptionMode[] + currentModeId?: string +}): SessionConfigOption[] { + const variants = variantsForModel(input.providers, input.currentModel) + const effort = buildEffortSelectOption({ variants, currentVariant: input.currentVariant }) + + return [ + buildModelSelectOption({ + providers: input.providers, + currentModel: input.currentModel, + currentVariant: input.currentVariant, + includeVariants: input.includeModelVariants ?? false, + }), + ...(effort ? [effort] : []), + ...(input.modes && input.currentModeId + ? [buildModeSelectOption({ modes: input.modes, currentModeId: input.currentModeId })] + : []), + ] +} + +export function parseModelSelection(modelId: string, providers: readonly ConfigOptionProvider[]): ModelSelection { + const provider = providers.find((item) => modelId.startsWith(`${item.id}/`)) + if (provider) { + const modelID = modelId.slice(provider.id.length + 1) + if (provider.models[modelID]) { + return { model: { providerID: provider.id, modelID } } + } + + const separator = modelID.lastIndexOf("/") + if (separator > -1) { + const baseModelID = modelID.slice(0, separator) + const variant = modelID.slice(separator + 1) + if (provider.models[baseModelID]?.variants?.[variant]) { + return { model: { providerID: provider.id, modelID: baseModelID }, variant } + } + } + + return { model: { providerID: provider.id, modelID } } + } + + const separator = modelId.indexOf("/") + if (separator === -1) { + return { model: { providerID: modelId, modelID: "" } } + } + + return { + model: { + providerID: modelId.slice(0, separator), + modelID: modelId.slice(separator + 1), + }, + } +} + +export function formatCurrentModelId(input: { + model: ModelSelection["model"] + variant?: string + variants?: readonly string[] + includeVariant?: boolean +}) { + const base = `${input.model.providerID}/${input.model.modelID}` + if (!input.includeVariant || !input.variants?.length) return base + return `${base}/${selectVariant(input.variant, input.variants)}` +} + +export function formatVariantName(variant: string) { + return variant + .split(/[_-]/) + .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part)) + .join(" ") +} + +function buildModelSelectOptions( + providers: readonly ConfigOptionProvider[], + options: { includeVariants: boolean }, +): Array<{ value: string; name: string }> { + return providers.flatMap((provider) => + Object.values(provider.models) + .sort((a, b) => a.name.localeCompare(b.name)) + .flatMap((model) => { + const base = { + value: `${provider.id}/${model.id}`, + name: `${provider.name}/${model.name}`, + } + if (!options.includeVariants || !model.variants) return [base] + + return [ + base, + ...Object.keys(model.variants) + .filter((variant) => variant !== DEFAULT_VARIANT_VALUE) + .map((variant) => ({ + value: `${provider.id}/${model.id}/${variant}`, + name: `${provider.name}/${model.name} (${formatVariantName(variant)})`, + })), + ] + }), + ) +} + +function variantsForModel(providers: readonly ConfigOptionProvider[], model: ModelSelection["model"]) { + return Object.keys( + providers.find((provider) => provider.id === model.providerID)?.models[model.modelID]?.variants ?? {}, + ) +} + +function selectVariant(variant: string | undefined, variants: readonly string[]) { + if (variant && variants.includes(variant)) return variant + if (variants.includes(DEFAULT_VARIANT_VALUE)) return DEFAULT_VARIANT_VALUE + return variants[0] +} diff --git a/packages/opencode/src/acp/content.ts b/packages/opencode/src/acp/content.ts new file mode 100644 index 00000000000..f83a75ef197 --- /dev/null +++ b/packages/opencode/src/acp/content.ts @@ -0,0 +1,250 @@ +import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk" +import path from "node:path" +import { pathToFileURL } from "node:url" +import type { MessageV2 } from "@/session/message-v2" + +export type PromptPart = MessageV2.TextPartInput | MessageV2.FilePartInput + +export type ReplayPart = + | { + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + } + | { + type: "file" + url: string + mime: string + filename?: string + } + | { + type: "reasoning" + text: string + } + +export function promptContentToParts(content: readonly ContentBlock[]): PromptPart[] { + return content.flatMap(contentBlockToParts) +} + +export function contentBlockToParts(block: ContentBlock): PromptPart[] { + switch (block.type) { + case "text": + return [ + { + type: "text", + text: block.text, + ...audienceFlags(block.annotations?.audience ?? undefined), + }, + ] + + case "image": + if (block.data) { + return [ + { + type: "file", + url: `data:${block.mimeType};base64,${block.data}`, + filename: filenameFromUri(block.uri ?? undefined) ?? "image", + mime: block.mimeType, + }, + ] + } + if (block.uri?.startsWith("data:")) { + return [ + { + type: "file", + url: block.uri, + filename: filenameFromUri(block.uri) ?? "image", + mime: block.mimeType, + }, + ] + } + if (block.uri?.startsWith("http://") || block.uri?.startsWith("https://")) { + return [ + { + type: "file", + url: block.uri, + filename: filenameFromUri(block.uri) ?? "image", + mime: block.mimeType, + }, + ] + } + return [] + + case "resource_link": + return [resourceLinkToPart(block)] + + case "resource": + if ("text" in block.resource) { + return [{ type: "text", text: block.resource.text }] + } + if (block.resource.mimeType) { + return [ + { + type: "file", + url: block.resource.uri.startsWith("data:") + ? block.resource.uri + : `data:${block.resource.mimeType};base64,${block.resource.blob}`, + filename: filenameFromUri(block.resource.uri) ?? "file", + mime: block.resource.mimeType, + }, + ] + } + return [] + + default: + return [] + } +} + +export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk[] { + return parts.flatMap(partToContentChunks) +} + +export function partToContentChunks(part: ReplayPart): ContentChunk[] { + switch (part.type) { + case "text": + if (!part.text) return [] + return [ + { + content: { + type: "text", + text: part.text, + ...partAudience(part), + }, + }, + ] + + case "file": + return filePartToContentChunks(part) + + case "reasoning": + if (!part.text) return [] + return [ + { + content: { + type: "text", + text: part.text, + }, + }, + ] + } +} + +function resourceLinkToPart(link: ResourceLink): PromptPart { + const parsed = uriToFilePart(link.uri, link.mimeType ?? "text/plain", link.name) + if (parsed.type === "file") return parsed + return { type: "text", text: parsed.text } +} + +function uriToFilePart( + uri: string, + mime: string, + filename?: string, +): MessageV2.FilePartInput | MessageV2.TextPartInput { + try { + if (uri.startsWith("file://")) { + return { + type: "file", + url: uri, + filename: filename ?? filenameFromUri(uri) ?? "file", + mime, + } + } + if (uri.startsWith("zed://")) { + const pathname = new URL(uri).searchParams.get("path") + if (pathname) { + return { + type: "file", + url: pathToFileURL(pathname).href, + filename: filename ?? (path.basename(pathname) || "file"), + mime, + } + } + } + return { type: "text", text: uri } + } catch { + return { type: "text", text: uri } + } +} + +function filePartToContentChunks(part: Extract): ContentChunk[] { + if (part.url.startsWith("file://")) { + return [ + { + content: { + type: "resource_link", + uri: part.url, + name: part.filename ?? "file", + mimeType: part.mime, + }, + }, + ] + } + if (!part.url.startsWith("data:")) return [] + + const data = decodeDataUrl(part.url) + if (!data) return [] + if (data.mime.startsWith("image/")) { + return [ + { + content: { + type: "image", + mimeType: data.mime, + data: data.base64, + uri: pathToFileURL(part.filename ?? "image").href, + }, + }, + ] + } + + return [ + { + content: { + type: "resource", + resource: + data.mime.startsWith("text/") || data.mime === "application/json" + ? { + uri: pathToFileURL(part.filename ?? "file").href, + mimeType: data.mime, + text: Buffer.from(data.base64, "base64").toString("utf8"), + } + : { + uri: pathToFileURL(part.filename ?? "file").href, + mimeType: data.mime, + blob: data.base64, + }, + }, + }, + ] +} + +function decodeDataUrl(url: string) { + const match = /^data:([^;]+);base64,(.*)$/.exec(url) + if (!match) return + return { mime: match[1], base64: match[2] } +} + +function audienceFlags(audience: readonly Role[] | null | undefined) { + if (audience?.length === 1 && audience[0] === "assistant") return { synthetic: true } + if (audience?.length === 1 && audience[0] === "user") return { ignored: true } + return {} +} + +function partAudience(part: Extract) { + const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined + if (!audience) return {} + return { annotations: { audience } } +} + +function filenameFromUri(uri: string | undefined) { + if (!uri) return + if (uri.startsWith("data:")) return + try { + const parsed = new URL(uri) + const name = path.basename(parsed.pathname) + return name || undefined + } catch { + return path.basename(uri) || undefined + } +} diff --git a/packages/opencode/src/acp/directory.ts b/packages/opencode/src/acp/directory.ts new file mode 100644 index 00000000000..c49613dd06f --- /dev/null +++ b/packages/opencode/src/acp/directory.ts @@ -0,0 +1,209 @@ +import { Agent } from "@/agent/agent" +import { Command } from "@/command" +import { InstanceRef } from "@/effect/instance-ref" +import { InstanceStore } from "@/project/instance-store" +import { ModelID, ProviderID } from "@/provider/schema" +import { Provider } from "@/provider/provider" +import { Context, Effect, Layer, SynchronizedRef } from "effect" +import type * as ACPError from "./error" + +export type ModelOption = { + readonly providerID: ProviderID + readonly providerName: string + readonly modelID: ModelID + readonly modelName: string +} + +export type ModeOption = { + readonly id: string + readonly name: string + readonly description?: string +} + +export type ModelVariants = NonNullable + +export type DefaultModel = { + readonly providerID: ProviderID + readonly modelID: ModelID +} + +export type Snapshot = { + readonly directory: string + readonly providers: Record + readonly modelOptions: readonly ModelOption[] + readonly variantsByModel: Readonly> + readonly availableModes: readonly ModeOption[] + readonly defaultModeID: string + readonly availableCommands: readonly Command.Info[] + readonly defaultModel?: DefaultModel +} + +export interface LoaderInterface { + readonly load: (directory: string) => Effect.Effect +} + +export interface Interface { + readonly get: (directory: string) => Effect.Effect + readonly refresh: (directory: string) => Effect.Effect + readonly variants: (snapshot: Snapshot, model: DefaultModel) => ModelVariants | undefined +} + +export class Loader extends Context.Service()("@opencode/ACPDirectoryLoader") {} + +export class Service extends Context.Service()("@opencode/ACPDirectory") {} + +export const modelKey = (model: DefaultModel) => `${model.providerID}/${model.modelID}` + +export const variants = (snapshot: Snapshot, model: DefaultModel) => snapshot.variantsByModel[modelKey(model)] + +export const build = (input: { + readonly directory: string + readonly providers: Record + readonly modes: readonly ModeOption[] + readonly defaultModeID: string + readonly commands: readonly Command.Info[] + readonly defaultModel?: DefaultModel +}): Snapshot => { + const modelOptions = Provider.sort( + Object.values(input.providers).flatMap((provider) => + Object.values(provider.models).map((model) => ({ + id: model.id, + providerID: provider.id, + providerName: provider.name, + modelID: model.id, + modelName: model.name, + })), + ), + ).map((model) => ({ + providerID: model.providerID, + providerName: model.providerName, + modelID: model.modelID, + modelName: model.modelName, + })) + + return { + directory: input.directory, + providers: input.providers, + modelOptions, + variantsByModel: Object.fromEntries( + Object.values(input.providers).flatMap((provider) => + Object.values(provider.models).flatMap((model) => + model.variants ? [[modelKey({ providerID: provider.id, modelID: model.id }), model.variants]] : [], + ), + ), + ), + availableModes: input.modes, + defaultModeID: input.modes.some((mode) => mode.id === input.defaultModeID) + ? input.defaultModeID + : (input.modes[0]?.id ?? input.defaultModeID), + availableCommands: input.commands, + ...(input.defaultModel ? { defaultModel: input.defaultModel } : {}), + } +} + +export const loaderLayer = Layer.effect( + Loader, + Effect.gen(function* () { + const store = yield* InstanceStore.Service + const provider = yield* Provider.Service + const agent = yield* Agent.Service + const command = yield* Command.Service + + return Loader.of({ + load: Effect.fn("ACPDirectoryLoader.load")(function* (directory) { + const ctx = yield* store.load({ directory }) + return yield* Effect.gen(function* () { + const providers = yield* provider.list() + const [agents, defaultAgent, commands, defaultModel] = yield* Effect.all( + [agent.list(), agent.defaultInfo(), command.list(), provider.defaultModel().pipe(Effect.option)], + { concurrency: "unbounded" }, + ) + return build({ + directory, + providers, + modes: agents + .filter((item) => item.mode !== "subagent" && item.hidden !== true) + .map((item) => ({ + id: item.name, + name: item.name, + ...(item.description ? { description: item.description } : {}), + })), + defaultModeID: defaultAgent.name, + commands: commands.toSorted((a, b) => a.name.localeCompare(b.name)), + ...(defaultModel._tag === "Some" ? { defaultModel: defaultModel.value } : {}), + }) + }).pipe(Effect.provideService(InstanceRef, ctx)) + }), + }) + }), +) + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const loader = yield* Loader + const snapshots = yield* SynchronizedRef.make(new Map>()) + + const cached = Effect.fnUntraced(function* (directory: string) { + return yield* SynchronizedRef.modifyEffect( + snapshots, + Effect.fnUntraced(function* (items) { + const current = items.get(directory) + if (current) return [current, items] as const + const next = yield* Effect.cached( + loader.load(directory).pipe( + Effect.tapError(() => + SynchronizedRef.update(snapshots, (state) => { + const next = new Map(state) + next.delete(directory) + return next + }), + ), + ), + ) + return [next, new Map(items).set(directory, next)] as const + }), + ) + }) + + const get = Effect.fn("ACPDirectory.get")(function* (directory: string) { + return yield* yield* cached(directory) + }) + + const refresh = Effect.fn("ACPDirectory.refresh")(function* (directory: string) { + return yield* SynchronizedRef.modifyEffect( + snapshots, + Effect.fnUntraced(function* (items) { + const next = yield* Effect.cached( + loader.load(directory).pipe( + Effect.tapError(() => + SynchronizedRef.update(snapshots, (state) => { + const next = new Map(state) + next.delete(directory) + return next + }), + ), + ), + ) + return [next, new Map(items).set(directory, next)] as const + }), + ).pipe(Effect.flatten) + }) + + return Service.of({ + get, + refresh, + variants, + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(loaderLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Agent.defaultLayer), + Layer.provide(Command.defaultLayer), + Layer.provide(InstanceStore.defaultLayer), +) + +export * as Directory from "./directory" diff --git a/packages/opencode/src/acp/error.ts b/packages/opencode/src/acp/error.ts new file mode 100644 index 00000000000..0c66a0e0498 --- /dev/null +++ b/packages/opencode/src/acp/error.ts @@ -0,0 +1,90 @@ +import { RequestError } from "@agentclientprotocol/sdk" +import { Schema } from "effect" + +export class SessionNotFoundError extends Schema.TaggedErrorClass()("ACPSessionNotFoundError", { + sessionId: Schema.String, +}) {} + +export class InvalidConfigOptionError extends Schema.TaggedErrorClass()( + "ACPInvalidConfigOptionError", + { + configId: Schema.String, + }, +) {} + +export class InvalidModelError extends Schema.TaggedErrorClass()("ACPInvalidModelError", { + modelId: Schema.String, + providerId: Schema.optional(Schema.String), +}) {} + +export class InvalidEffortError extends Schema.TaggedErrorClass()("ACPInvalidEffortError", { + effort: Schema.String, +}) {} + +export class InvalidModeError extends Schema.TaggedErrorClass()("ACPInvalidModeError", { + mode: Schema.String, +}) {} + +export class AuthRequiredError extends Schema.TaggedErrorClass()("ACPAuthRequiredError", { + providerId: Schema.optional(Schema.String), +}) {} + +export class UnknownAuthMethodError extends Schema.TaggedErrorClass()( + "ACPUnknownAuthMethodError", + { + methodId: Schema.String, + }, +) {} + +export class UnsupportedOperationError extends Schema.TaggedErrorClass()( + "ACPUnsupportedOperationError", + { + method: Schema.String, + }, +) {} + +export class ServiceFailureError extends Schema.TaggedErrorClass()("ACPServiceFailureError", { + safeMessage: Schema.String, + service: Schema.optional(Schema.String), +}) {} + +export type Error = + | SessionNotFoundError + | InvalidConfigOptionError + | InvalidModelError + | InvalidEffortError + | InvalidModeError + | AuthRequiredError + | UnknownAuthMethodError + | UnsupportedOperationError + | ServiceFailureError + +export function toRequestError(error: Error) { + switch (error._tag) { + case "ACPSessionNotFoundError": + return RequestError.invalidParams({ sessionId: error.sessionId }, `session not found: ${error.sessionId}`) + case "ACPInvalidConfigOptionError": + return RequestError.invalidParams({ configId: error.configId }, `unknown config option: ${error.configId}`) + case "ACPInvalidModelError": + return RequestError.invalidParams( + { providerId: error.providerId, modelId: error.modelId }, + `model not found: ${error.modelId}`, + ) + case "ACPInvalidEffortError": + return RequestError.invalidParams({ effort: error.effort }, `effort not found: ${error.effort}`) + case "ACPInvalidModeError": + return RequestError.invalidParams({ mode: error.mode }, `mode not found: ${error.mode}`) + case "ACPAuthRequiredError": + return RequestError.authRequired({ providerId: error.providerId }, "provider authentication required") + case "ACPUnknownAuthMethodError": + return RequestError.invalidParams({ methodId: error.methodId }, `unknown auth method: ${error.methodId}`) + case "ACPUnsupportedOperationError": + return RequestError.methodNotFound(error.method) + case "ACPServiceFailureError": + return RequestError.internalError({ service: error.service }, error.safeMessage) + } +} + +export function fromUnknownDefect(_defect: unknown, safeMessage = "Internal service failure") { + return new ServiceFailureError({ safeMessage }) +} diff --git a/packages/opencode/src/acp/event.ts b/packages/opencode/src/acp/event.ts new file mode 100644 index 00000000000..32f2ecce295 --- /dev/null +++ b/packages/opencode/src/acp/event.ts @@ -0,0 +1,319 @@ +import type { AgentSideConnection } from "@agentclientprotocol/sdk" +import * as Log from "@opencode-ai/core/util/log" +import type { + Event, + EventMessagePartDelta, + EventMessagePartUpdated, + KiloClient, + Part, + SessionMessageResponse, + ToolPart, +} from "@kilocode/sdk/v2" +import { Effect } from "effect" +import { ACPSession } from "./session" +import { ACPPermission } from "./permission" +import { + duplicateRunningToolUpdate, + errorToolUpdate, + pendingToolCall, + runningToolUpdate, + shellOutputSnapshot, + completedToolUpdate, +} from "./tool" + +const log = Log.create({ service: "acp-event" }) + +type Connection = Pick & + Partial> +type GlobalEventEnvelope = { + payload?: Event +} +type GlobalEventStream = { + stream: AsyncIterable +} + +export function start(input: { sdk: KiloClient; connection: Connection; session: ACPSession.Interface }) { + const subscription = new Subscription(input) + subscription.start() + return subscription +} + +export class Subscription { + private readonly abort = new AbortController() + private readonly shellSnapshots = new Map() + private readonly toolStarts = new Set() + private readonly permission: ACPPermission.Handler + private started = false + + constructor( + private readonly input: { + sdk: KiloClient + connection: Connection + session: ACPSession.Interface + }, + ) { + this.permission = new ACPPermission.Handler(input) + } + + start() { + if (this.started) return + this.started = true + this.run().catch((error: unknown) => { + if (this.abort.signal.aborted) return + log.error("event subscription failed", { error }) + }) + } + + stop() { + this.abort.abort() + } + + async handle(event: Event) { + switch (event.type) { + case "permission.asked": + this.permission.handle(event) + return + case "message.part.updated": + return this.handlePartUpdated(event) + case "message.part.delta": + return this.handlePartDelta(event) + } + } + + async replayMessage(message: SessionMessageResponse) { + if (message.info.role !== "assistant" && message.info.role !== "user") return + + for (const part of message.parts) { + await this.recordFetchedPart(message.info.sessionID, message, part) + if (part.type === "tool") { + await this.handleToolPart(message.info.sessionID, part) + } + } + } + + private async run() { + while (!this.abort.signal.aborted) { + const events = (await this.input.sdk.global.event({ + signal: this.abort.signal, + })) as GlobalEventStream + + for await (const event of events.stream) { + if (this.abort.signal.aborted) return + if (!event.payload) continue + await this.handle(event.payload).catch((error: unknown) => { + log.error("failed to handle event", { error, type: event.payload?.type }) + }) + } + if (!this.abort.signal.aborted) await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } + + private async handlePartUpdated(event: EventMessagePartUpdated) { + const part = event.properties.part + const sessionId = part.sessionID || event.properties.sessionID + const session = await Effect.runPromise(this.input.session.tryGet(sessionId)) + if (!session) return + + await Effect.runPromise( + this.input.session.recordPartMetadata({ + sessionId: session.id, + messageId: part.messageID, + partId: part.id, + partType: part.type, + role: part.type === "reasoning" ? "assistant" : undefined, + ignored: part.type === "text" ? part.ignored : undefined, + toolCallId: part.type === "tool" ? part.callID : undefined, + metadata: "metadata" in part ? part.metadata : undefined, + }), + ) + if (part.type === "tool") { + await this.handleToolPart(session.id, part) + } + } + + private async handlePartDelta(event: EventMessagePartDelta) { + const props = event.properties + const session = await Effect.runPromise(this.input.session.tryGet(props.sessionID)) + if (!session) return + + const known = await Effect.runPromise( + this.input.session.tryGetPartMetadata({ + sessionId: session.id, + messageId: props.messageID, + partId: props.partID, + }), + ) + const metadata = + known?.role && known.partType + ? known + : await this.fetchPartMetadata(session.id, session.cwd, props.messageID, props.partID) + if (metadata?.role !== "assistant") return + if (metadata.partType === "text" && props.field === "text" && metadata.ignored !== true) { + await this.input.connection.sessionUpdate({ + sessionId: session.id, + update: { + sessionUpdate: "agent_message_chunk", + messageId: props.messageID, + content: { + type: "text", + text: props.delta, + }, + }, + }) + return + } + + if (metadata.partType === "reasoning" && props.field === "text") { + await this.input.connection.sessionUpdate({ + sessionId: session.id, + update: { + sessionUpdate: "agent_thought_chunk", + messageId: props.messageID, + content: { + type: "text", + text: props.delta, + }, + }, + }) + } + } + + private async fetchPartMetadata(sessionId: string, cwd: string, messageId: string, partId: string) { + const message = await this.input.sdk.session + .message( + { + sessionID: sessionId, + messageID: messageId, + directory: cwd, + }, + { throwOnError: true }, + ) + .then((response) => response.data) + .catch((error: unknown) => { + log.error("unexpected error when fetching message for delta metadata", { error, messageId, partId }) + return undefined + }) + if (!message) return + + const part = message.parts.find((item) => item.id === partId) + if (!part) return + return await this.recordFetchedPart(sessionId, message, part) + } + + private async recordFetchedPart(sessionId: string, message: SessionMessageResponse, part: Part) { + return await Effect.runPromise( + this.input.session.recordPartMetadata({ + sessionId, + messageId: part.messageID, + partId: part.id, + partType: part.type, + role: message.info.role, + ignored: part.type === "text" ? part.ignored : undefined, + toolCallId: part.type === "tool" ? part.callID : undefined, + metadata: "metadata" in part ? part.metadata : undefined, + }), + ) + } + + private async handleToolPart(sessionId: string, part: ToolPart) { + await this.toolStart(sessionId, part) + + switch (part.state.status) { + case "pending": + this.shellSnapshots.delete(part.callID) + return + + case "running": + await this.runningTool(sessionId, part) + return + + case "completed": + this.clearTool(part.callID) + await this.input.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + ...completedToolUpdate({ + toolCallId: part.callID, + toolName: part.tool, + state: part.state, + }), + }, + }) + return + + case "error": + this.clearTool(part.callID) + await this.input.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + ...errorToolUpdate({ + toolCallId: part.callID, + toolName: part.tool, + state: part.state, + }), + }, + }) + return + } + } + + private async runningTool(sessionId: string, part: ToolPart) { + if (part.state.status !== "running") return + + const output = part.tool === "bash" ? shellOutputSnapshot(part.state) : undefined + if (output !== undefined) { + if (this.shellSnapshots.get(part.callID) === output) { + await this.input.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + ...duplicateRunningToolUpdate({ + toolCallId: part.callID, + toolName: part.tool, + state: part.state, + }), + }, + }) + return + } + this.shellSnapshots.set(part.callID, output) + } + + await this.input.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call_update", + ...runningToolUpdate({ + toolCallId: part.callID, + toolName: part.tool, + state: part.state, + output, + }), + }, + }) + } + + private async toolStart(sessionId: string, part: ToolPart) { + if (this.toolStarts.has(part.callID)) return + this.toolStarts.add(part.callID) + await this.input.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "tool_call", + ...pendingToolCall({ + toolCallId: part.callID, + toolName: part.tool, + }), + }, + }) + } + + private clearTool(toolCallId: string) { + this.toolStarts.delete(toolCallId) + this.shellSnapshots.delete(toolCallId) + } +} + +export * as ACPEvent from "./event" diff --git a/packages/opencode/src/acp/permission.ts b/packages/opencode/src/acp/permission.ts new file mode 100644 index 00000000000..b79fac75864 --- /dev/null +++ b/packages/opencode/src/acp/permission.ts @@ -0,0 +1,145 @@ +import type { AgentSideConnection, PermissionOption, RequestPermissionResponse } from "@agentclientprotocol/sdk" +import * as Log from "@opencode-ai/core/util/log" +import type { Event, KiloClient } from "@kilocode/sdk/v2" +import { applyPatch } from "diff" +import { exists, readText } from "@/util/filesystem" +import type { ACPSession } from "./session" +import { toLocations, toToolKind, type ToolInput } from "./tool" +import { Effect } from "effect" + +const log = Log.create({ service: "acp-permission" }) + +type PermissionEvent = Extract +type Reply = "once" | "always" | "reject" +type Connection = Partial> + +const permissionOptions: PermissionOption[] = [ + { optionId: "once", kind: "allow_once", name: "Allow once" }, + { optionId: "always", kind: "allow_always", name: "Always allow" }, + { optionId: "reject", kind: "reject_once", name: "Reject" }, +] + +export class Handler { + private readonly queues = new Map>() + + constructor( + private readonly input: { + sdk: KiloClient + connection: Connection + session: ACPSession.Interface + }, + ) {} + + handle(event: PermissionEvent) { + const permission = event.properties + const previous = this.queues.get(permission.sessionID) ?? Promise.resolve() + const next = previous + .then(() => this.process(event)) + .catch((error: unknown) => { + log.error("failed to handle permission", { error, permissionID: permission.id }) + }) + .finally(() => { + if (this.queues.get(permission.sessionID) === next) { + this.queues.delete(permission.sessionID) + } + }) + this.queues.set(permission.sessionID, next) + } + + private async process(event: PermissionEvent) { + const permission = event.properties + const session = await Effect.runPromise(this.input.session.tryGet(permission.sessionID)) + if (!session) return + + if (!this.input.connection.requestPermission) { + log.error("ACP connection cannot request permission", { + permissionID: permission.id, + sessionID: permission.sessionID, + }) + await this.reply(permission.id, "reject", session.cwd) + return + } + + const result = await this.input.connection + .requestPermission({ + sessionId: permission.sessionID, + toolCall: { + toolCallId: permission.tool?.callID ?? permission.id, + status: "pending", + title: permission.permission, + rawInput: permission.metadata, + kind: toToolKind(permission.permission), + locations: toLocations(permission.permission, permission.metadata), + }, + options: permissionOptions, + }) + .catch(async (error: unknown) => { + log.error("failed to request permission from ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, + }) + await this.reply(permission.id, "reject", session.cwd) + return undefined + }) + + if (!result) return + + const reply = selectedReply(result) + if (reply !== "once" && reply !== "always") { + await this.reply(permission.id, "reject", session.cwd) + return + } + + if (permission.permission === "edit") { + await this.writeProposedEdit(session.id, permission.metadata).catch((error: unknown) => { + log.error("failed to write proposed edit through ACP", { + error, + permissionID: permission.id, + sessionID: permission.sessionID, + }) + }) + } + + await this.reply(permission.id, reply, session.cwd) + } + + private async reply(requestID: string, reply: Reply, directory: string) { + await this.input.sdk.permission.reply({ + requestID, + reply, + directory, + }) + } + + private async writeProposedEdit(sessionId: string, metadata: ToolInput) { + const filepath = stringValue(metadata.filepath) + const diff = stringValue(metadata.diff) + if (!filepath || !diff || !this.input.connection.writeTextFile) return + + const content = (await exists(filepath)) ? await readText(filepath) : "" + const next = applyPatch(content, diff) + if (next === false) { + log.error("Failed to apply unified diff (context mismatch)") + return + } + + void this.input.connection.writeTextFile({ + sessionId, + path: filepath, + content: next, + }) + } +} + +function selectedReply(result: RequestPermissionResponse): Reply { + if (result.outcome.outcome !== "selected") return "reject" + if (result.outcome.optionId === "once" || result.outcome.optionId === "always") return result.outcome.optionId + return "reject" +} + +function stringValue(value: unknown) { + return typeof value === "string" ? value : undefined +} + +export * as ACPPermission from "./permission" diff --git a/packages/opencode/src/acp/profile.ts b/packages/opencode/src/acp/profile.ts new file mode 100644 index 00000000000..4d6b184a902 --- /dev/null +++ b/packages/opencode/src/acp/profile.ts @@ -0,0 +1,42 @@ +const enabled = process.env.KILO_ACP_PROFILE === "1" +const started = performance.now() + +export function mark(name: string, fields?: Record) { + if (!enabled) return + write(`${name}.mark`, performance.now() - started, fields) +} + +export function duration( + name: string, + startedAt: number, + fields?: Record, +) { + if (!enabled) return + write(name, performance.now() - startedAt, fields) +} + +export async function measure( + name: string, + fn: () => Promise, + fields?: Record, +) { + if (!enabled) return fn() + const start = performance.now() + try { + return await fn() + } finally { + write(name, performance.now() - start, fields) + } +} + +function write(name: string, durationMs: number, fields?: Record) { + const extra = fields + ? Object.entries(fields) + .filter((entry): entry is [string, string | number | boolean] => entry[1] !== undefined) + .map(([key, value]) => `${key}=${value}`) + .join(" ") + : "" + console.error(`[acp-profile] ${name} ${Math.round(durationMs)}ms${extra ? ` ${extra}` : ""}`) +} + +export * as ACPProfile from "./profile" diff --git a/packages/opencode/src/acp/runtime.ts b/packages/opencode/src/acp/runtime.ts deleted file mode 100644 index b08c73cf2e2..00000000000 --- a/packages/opencode/src/acp/runtime.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Agent } from "@/agent/agent" -import { AppRuntime, type AppServices } from "@/effect/app-runtime" -import { InstanceRef } from "@/effect/instance-ref" -import { InstanceRuntime } from "@/project/instance-runtime" -import { Effect } from "effect" - -// Global ACP Effect re-entry: no project InstanceRef is provided. -export const runGlobal = AppRuntime.runPromise - -// Directory-scoped ACP Effect re-entry: load the project instance and provide InstanceRef. -export async function runDirectory(input: { directory: string; effect: Effect.Effect }) { - const ctx = await InstanceRuntime.load({ directory: input.directory }) - return AppRuntime.runPromise(input.effect.pipe(Effect.provideService(InstanceRef, ctx))) -} - -export const defaultAgentInfo = (directory: string) => - runDirectory({ - directory, - effect: Agent.Service.use((svc) => svc.defaultInfo()), - }) - -export * as ACPRuntime from "./runtime" diff --git a/packages/opencode/src/acp/service.ts b/packages/opencode/src/acp/service.ts new file mode 100644 index 00000000000..9bfe8147841 --- /dev/null +++ b/packages/opencode/src/acp/service.ts @@ -0,0 +1,1060 @@ +import { + type AgentSideConnection, + type AuthenticateRequest, + type AuthenticateResponse, + type AuthMethod, + type CancelNotification, + type CloseSessionRequest, + type CloseSessionResponse, + type ForkSessionRequest, + type ForkSessionResponse, + type InitializeRequest, + type InitializeResponse, + type ListSessionsRequest, + type ListSessionsResponse, + type LoadSessionRequest, + type LoadSessionResponse, + type McpServer, + type NewSessionRequest, + type NewSessionResponse, + type PromptRequest, + type PromptResponse, + type ResumeSessionRequest, + type ResumeSessionResponse, + type SessionInfo, + type SetSessionConfigOptionRequest, + type SetSessionConfigOptionResponse, + type SetSessionModelRequest, + type SetSessionModelResponse, + type SetSessionModeRequest, + type SetSessionModeResponse, +} from "@agentclientprotocol/sdk" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import * as Log from "@opencode-ai/core/util/log" +import type { Message, KiloClient, SessionMessageResponse } from "@kilocode/sdk/v2" +import { Context, Effect, Layer, ManagedRuntime } from "effect" +import * as ACPError from "./error" +import { buildConfigOptions, parseModelSelection } from "./config-option" +import { promptContentToParts } from "./content" +import { Directory } from "./directory" +import { ACPEvent } from "./event" +import { ACPSession } from "./session" +import { UsageService } from "./usage" +import { ACPProfile } from "./profile" +import { ModelID, ProviderID } from "@/provider/schema" +import { Provider } from "@/provider/provider" +import type { Command } from "@/command" + +export const AuthMethodID = "kilo-login" // kilocode_change +const log = Log.create({ service: "acp-service" }) + +export type Error = ACPError.Error +type ServiceConnection = Pick & + Partial> + +export type Interface = { + readonly initialize: (input: InitializeRequest) => Effect.Effect + readonly authenticate: (input: AuthenticateRequest) => Effect.Effect + readonly newSession: (input: NewSessionRequest) => Effect.Effect + readonly loadSession: (input: LoadSessionRequest) => Effect.Effect + readonly listSessions: (input: ListSessionsRequest) => Effect.Effect + readonly resumeSession: (input: ResumeSessionRequest) => Effect.Effect + readonly closeSession: (input: CloseSessionRequest) => Effect.Effect + readonly forkSession: (input: ForkSessionRequest) => Effect.Effect + readonly setSessionConfigOption: ( + input: SetSessionConfigOptionRequest, + ) => Effect.Effect + readonly setSessionMode: (input: SetSessionModeRequest) => Effect.Effect + readonly setSessionModel: (input: SetSessionModelRequest) => Effect.Effect + readonly prompt: (input: PromptRequest) => Effect.Effect + readonly cancel: (input: CancelNotification) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ACP/Service") {} + +export function make(input: { + sdk: KiloClient + connection?: ServiceConnection + directory?: Directory.Interface + session?: ACPSession.Interface + usage?: UsageService.Interface + eventSubscription?: (subscription: ACPEvent.Subscription) => void +}): Interface { + const session = input.session ?? makeSessionService() + const directoryService = input.directory ?? makeDirectoryService(input.sdk) + const registeredMcp = new Map>() + const sessionSnapshots = new Map() + const events = input.connection + ? ACPEvent.start({ sdk: input.sdk, connection: input.connection, session }) + : undefined + if (events) input.eventSubscription?.(events) + + const initialize = Effect.fn("ACP.initialize")(function* (params: InitializeRequest) { + const started = performance.now() + // kilocode_change start + const authMethod: AuthMethod = { + description: "Run `kilo auth login` in the terminal", + name: "Login with Kilo", + id: AuthMethodID, + } + // kilocode_change end + + if (params.clientCapabilities?._meta?.["terminal-auth"] === true) { + authMethod._meta = { + "terminal-auth": { + command: "opencode", + args: ["auth", "login"], + label: "Kilo Login", + }, + } + } + + const response = { + protocolVersion: 1, + agentCapabilities: { + loadSession: true, + mcpCapabilities: { + http: true, + sse: true, + }, + promptCapabilities: { + embeddedContext: true, + image: true, + }, + sessionCapabilities: { + close: {}, + fork: {}, + list: {}, + resume: {}, + }, + }, + authMethods: [authMethod], + agentInfo: { + name: "Kilo", + version: InstallationVersion, + }, + } + ACPProfile.duration("acp.initialize", started) + return response + }) + + const authenticate = Effect.fn("ACP.authenticate")(function* (params: AuthenticateRequest) { + if (params.methodId !== AuthMethodID) { + return yield* new ACPError.UnknownAuthMethodError({ methodId: params.methodId }) + } + return {} + }) + + const directorySnapshot = Effect.fn("ACP.directorySnapshot")(function* (cwd: string) { + const started = performance.now() + const snapshot = yield* directoryService.get(cwd) + ACPProfile.duration("acp.directory.snapshot", started) + return snapshot + }) + + const configSnapshot = Effect.fn("ACP.configSnapshot")(function* (state: ACPSession.Info) { + const snapshot = sessionSnapshots.get(state.id) + if (snapshot) return snapshot + const loaded = yield* directorySnapshot(state.cwd) + sessionSnapshots.set(state.id, loaded) + return loaded + }) + + const newSession = Effect.fn("ACP.newSession")(function* (params: NewSessionRequest) { + const started = performance.now() + const snapshot = yield* directorySnapshot(params.cwd) + const selected = selectDefaultModel(snapshot) + const variant = selectVariant(snapshot, selected) + const modeId = snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined + const created = yield* profiledRequest( + "acp.newSession.session.create", + () => + input.sdk.session.create( + { + directory: params.cwd, + ...(modeId ? { agent: modeId } : {}), + model: { + providerID: selected.providerID, + id: selected.modelID, + ...(variant ? { variant } : {}), + }, + }, + { throwOnError: true }, + ), + "session", + ) + const state = yield* session.create({ + id: created.id, + cwd: params.cwd, + mcpServers: params.mcpServers, + model: selected, + variant, + modeId, + }) + sessionSnapshots.set(state.id, snapshot) + + yield* registerMcpServers(input.sdk, registeredMcp, params.cwd, state.id, params.mcpServers) + yield* sendAvailableCommands(input.connection, state.id, snapshot) + + const response = { + sessionId: state.id, + configOptions: configOptions(snapshot, { + model: state.model ?? selected, + variant: state.variant, + modeId: state.modeId, + }), + } + ACPProfile.duration("acp.newSession", started) + return response + }) + + const loadSession = Effect.fn("ACP.loadSession")(function* (params: LoadSessionRequest) { + const snapshot = yield* directorySnapshot(params.cwd) + yield* request( + () => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }), + "session", + ) + const messages = yield* request( + () => + input.sdk.session.messages( + { directory: params.cwd, sessionID: params.sessionId, limit: 100 }, + { throwOnError: true }, + ), + "session", + ) + const restored = restoreFromMessages(messages.map((item) => item.info)) + const model = restored.model ?? selectDefaultModel(snapshot) + const state = yield* session.load({ + id: params.sessionId, + cwd: params.cwd, + mcpServers: params.mcpServers, + model, + variant: restored.variant ?? selectVariant(snapshot, model), + modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined), + }) + sessionSnapshots.set(state.id, snapshot) + + yield* registerMcpServers(input.sdk, registeredMcp, params.cwd, state.id, params.mcpServers) + yield* sendAvailableCommands(input.connection, state.id, snapshot) + yield* replayMessages(events, messages) + + return { + configOptions: configOptions(snapshot, { + model: state.model ?? model, + variant: state.variant, + modeId: state.modeId, + }), + } + }) + + const listSessions = Effect.fn("ACP.listSessions")(function* (params: ListSessionsRequest) { + const cursor = params.cursor ? Number(params.cursor) : undefined + const limit = 100 + const sessions = yield* request( + () => + input.sdk.session.list( + { + ...(params.cwd ? { directory: params.cwd } : {}), + roots: true, + }, + { throwOnError: true }, + ), + "session", + ) + const serverEntries = sessions.map( + (item): SessionInfo => ({ + sessionId: item.id, + cwd: item.directory, + title: item.title, + updatedAt: new Date(item.time.updated).toISOString(), + }), + ) + const liveEntries = (yield* session.list(params.cwd ?? undefined)) + .filter((item) => !serverEntries.some((entry) => entry.sessionId === item.id)) + .map( + (item): SessionInfo => ({ + sessionId: item.id, + cwd: item.cwd, + updatedAt: item.createdAt.toISOString(), + }), + ) + const sorted = [...liveEntries, ...serverEntries].toSorted( + (a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime(), + ) + const filtered = + cursor === undefined || !Number.isFinite(cursor) + ? sorted + : sorted.filter((item) => new Date(item.updatedAt ?? 0).getTime() < cursor) + const page = filtered.slice(0, limit) + const last = page.at(-1) + return { + sessions: page, + ...(filtered.length > limit && last ? { nextCursor: String(new Date(last.updatedAt ?? 0).getTime()) } : {}), + } + }) + + const resumeSession = Effect.fn("ACP.resumeSession")(function* (params: ResumeSessionRequest) { + const snapshot = yield* directorySnapshot(params.cwd) + yield* request( + () => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }), + "session", + ) + const messages = yield* request( + () => + input.sdk.session.messages( + { directory: params.cwd, sessionID: params.sessionId, limit: 20 }, + { throwOnError: true }, + ), + "session", + ) + const restored = restoreFromMessages(messages.map((item) => item.info)) + const model = restored.model ?? selectDefaultModel(snapshot) + const state = yield* session.load({ + id: params.sessionId, + cwd: params.cwd, + mcpServers: params.mcpServers ?? [], + model, + variant: restored.variant ?? selectVariant(snapshot, model), + modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined), + }) + sessionSnapshots.set(state.id, snapshot) + + yield* registerMcpServers(input.sdk, registeredMcp, params.cwd, state.id, params.mcpServers ?? []) + yield* sendAvailableCommands(input.connection, state.id, snapshot) + yield* replayMessages(events, messages) + + return { + configOptions: configOptions(snapshot, { + model: state.model ?? model, + variant: state.variant, + modeId: state.modeId, + }), + } + }) + + const closeSession = Effect.fn("ACP.closeSession")(function* (params: CloseSessionRequest) { + const removed = yield* session.remove(params.sessionId) + registeredMcp.delete(params.sessionId) + sessionSnapshots.delete(params.sessionId) + if (!removed) return {} + + yield* request( + () => input.sdk.session.abort({ directory: removed.cwd, sessionID: params.sessionId }, { throwOnError: true }), + "session", + ).pipe( + Effect.catch((error) => + Effect.sync(() => { + log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId }) + }), + ), + ) + return {} + }) + + const forkSession = Effect.fn("ACP.forkSession")(function* (params: ForkSessionRequest) { + const snapshot = yield* directorySnapshot(params.cwd) + const forked = yield* request( + () => + input.sdk.session.fork( + { + directory: params.cwd, + sessionID: params.sessionId, + }, + { throwOnError: true }, + ), + "session", + ) + const messages = yield* request( + () => + input.sdk.session.messages({ directory: params.cwd, sessionID: forked.id, limit: 20 }, { throwOnError: true }), + "session", + ) + const restored = restoreFromMessages(messages.map((item) => item.info)) + const model = restored.model ?? selectDefaultModel(snapshot) + const state = yield* session.load({ + id: forked.id, + cwd: params.cwd, + mcpServers: params.mcpServers ?? [], + model, + variant: restored.variant ?? selectVariant(snapshot, model), + modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined), + }) + sessionSnapshots.set(state.id, snapshot) + + yield* registerMcpServers(input.sdk, registeredMcp, params.cwd, state.id, params.mcpServers ?? []) + yield* sendAvailableCommands(input.connection, state.id, snapshot) + yield* replayMessages(events, messages) + + return { + sessionId: state.id, + configOptions: configOptions(snapshot, { + model: state.model ?? model, + variant: state.variant, + modeId: state.modeId, + }), + } + }) + + const setSessionConfigOption = Effect.fn("ACP.setSessionConfigOption")(function* ( + params: SetSessionConfigOptionRequest, + ) { + const current = yield* session.get(params.sessionId) + const snapshot = yield* configSnapshot(current) + if (typeof params.value !== "string") { + return yield* new ACPError.InvalidConfigOptionError({ configId: params.configId }) + } + + if (params.configId === "model") { + const selected = yield* parseSelectedModel(snapshot, params.value) + const variant = selected.variant ?? selectVariant(snapshot, selected.model) + const state = yield* session + .setVariant(params.sessionId, Directory.variants(snapshot, selected.model) ? variant : undefined) + .pipe(Effect.andThen(session.setModel(params.sessionId, selected.model))) + return { + configOptions: configOptions(snapshot, { + model: state.model ?? selected.model, + variant: state.variant, + modeId: state.modeId, + }), + } + } + + if (params.configId === "effort") { + const model = current.model ?? selectDefaultModel(snapshot) + const variants = Directory.variants(snapshot, model) + if (!variants || !Object.keys(variants).includes(params.value)) { + return yield* new ACPError.InvalidEffortError({ effort: params.value }) + } + const state = yield* session.setVariant(params.sessionId, params.value) + return { + configOptions: configOptions(snapshot, { + model: state.model ?? model, + variant: state.variant, + modeId: state.modeId, + }), + } + } + + if (params.configId === "mode") { + if (!snapshot.availableModes.some((mode) => mode.id === params.value)) { + return yield* new ACPError.InvalidModeError({ mode: params.value }) + } + const state = yield* session.setMode(params.sessionId, params.value) + return { + configOptions: configOptions(snapshot, { + model: state.model ?? selectDefaultModel(snapshot), + variant: state.variant, + modeId: state.modeId, + }), + } + } + + return yield* new ACPError.InvalidConfigOptionError({ configId: params.configId }) + }) + + const setSessionMode = Effect.fn("ACP.setSessionMode")(function* (params: SetSessionModeRequest) { + const current = yield* session.get(params.sessionId) + const snapshot = yield* configSnapshot(current) + if (!snapshot.availableModes.some((mode) => mode.id === params.modeId)) { + return yield* new ACPError.InvalidModeError({ mode: params.modeId }) + } + yield* session.setMode(params.sessionId, params.modeId) + return {} + }) + + const setSessionModel = Effect.fn("ACP.setSessionModel")(function* (params: SetSessionModelRequest) { + const current = yield* session.get(params.sessionId) + const snapshot = yield* configSnapshot(current) + const selected = yield* parseSelectedModel(snapshot, params.modelId) + yield* session + .setVariant( + params.sessionId, + Directory.variants(snapshot, selected.model) + ? (selected.variant ?? selectVariant(snapshot, selected.model)) + : undefined, + ) + .pipe(Effect.andThen(session.setModel(params.sessionId, selected.model))) + return {} + }) + + return { + initialize, + authenticate, + newSession, + loadSession, + listSessions, + resumeSession, + closeSession, + forkSession, + setSessionConfigOption, + setSessionMode, + setSessionModel, + prompt: Effect.fn("ACP.prompt")(function* (params: PromptRequest) { + const current = yield* session.get(params.sessionId) + const snapshot = yield* directorySnapshot(current.cwd) + const selected = current.model ?? selectDefaultModel(snapshot) + if (!current.model) { + yield* session.setModel(params.sessionId, selected) + } + const variant = current.variant ?? selectVariant(snapshot, selected) + const modeId = current.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined) + const parts = promptContentToParts(params.prompt) + const command = detectSlashCommand(parts) + + if (!command) { + const response = yield* request( + () => + input.sdk.session.prompt( + { + sessionID: current.id, + model: { + providerID: selected.providerID, + modelID: selected.modelID, + }, + ...(variant ? { variant } : {}), + parts, + ...(modeId ? { agent: modeId } : {}), + directory: current.cwd, + }, + { throwOnError: true }, + ), + "session", + ) + yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd) + return promptResponse(response.info, params.messageId) + } + + const known = snapshot.availableCommands.find((item) => item.name === command.name) + if (known) { + const response = yield* request( + () => + input.sdk.session.command( + { + sessionID: current.id, + command: known.name, + arguments: command.args, + model: `${selected.providerID}/${selected.modelID}`, + ...(variant ? { variant } : {}), + ...(modeId ? { agent: modeId } : {}), + directory: current.cwd, + }, + { throwOnError: true }, + ), + "session", + ) + yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd) + return promptResponse(response.info, params.messageId) + } + + if (command.name === "compact") { + yield* request( + () => + input.sdk.session.summarize( + { + sessionID: current.id, + directory: current.cwd, + providerID: selected.providerID, + modelID: selected.modelID, + }, + { throwOnError: true }, + ), + "session", + ) + } + + yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd) + return promptResponse(undefined, params.messageId) + }), + cancel: Effect.fn("ACP.cancel")(function* (_input: CancelNotification) { + return yield* new ACPError.UnsupportedOperationError({ method: "session/cancel" }) + }), + } +} + +function makeSessionService() { + return ManagedRuntime.make(ACPSession.defaultLayer).runSync( + ACPSession.Service.use((service) => Effect.succeed(service)), + ) +} + +function makeDirectoryService(sdk: KiloClient) { + return ManagedRuntime.make( + Directory.layer.pipe( + Layer.provide( + Layer.succeed( + Directory.Loader, + Directory.Loader.of({ + load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"), + }), + ), + ), + ), + ).runSync(Directory.Service.use((service) => Effect.succeed(service))) +} + +function makeUsageService(sdk: KiloClient) { + const limits = new Map>() + const contextLimit: UsageService.Interface["contextLimit"] = Effect.fn("ACP.promptUsage.contextLimit")( + function* (params) { + const key = `${params.directory}\u0000${params.providerID}\u0000${params.modelID}` + const current = limits.get(key) + if (current) return yield* Effect.promise(() => current) + + const next = sdk.config + .providers({ directory: params.directory }, { throwOnError: true }) + .then((response) => { + const providers = Object.fromEntries( + (response.data?.providers ?? []).map((provider) => [provider.id, provider]), + ) as Record + return UsageService.findContextLimit(providers, params.providerID, params.modelID) + }) + .catch((error: unknown) => { + log.error("failed to get providers for usage context limit", { error }) + return undefined + }) + limits.set(key, next) + return yield* Effect.promise(() => next) + }, + ) + + const sendUpdate: UsageService.Interface["sendUpdate"] = Effect.fn("ACP.promptUsage.sendUpdate")(function* (params) { + const messages = yield* request( + () => + sdk.session.messages( + { + sessionID: params.sessionID, + directory: params.directory, + }, + { throwOnError: true }, + ), + "session", + ).pipe( + Effect.map((messages) => messages as readonly UsageService.SessionMessage[]), + Effect.catch((error) => + Effect.sync(() => { + log.error("failed to fetch messages for usage update", { error }) + return undefined + }), + ), + ) + if (!messages) return + + const message = UsageService.latestAssistantMessage(messages) + if (!message?.providerID || !message.modelID) return + + const size = yield* contextLimit({ + directory: params.directory, + providerID: ProviderID.make(message.providerID), + modelID: ModelID.make(message.modelID), + }) + if (!size) return + + yield* Effect.promise(() => + params.connection + .sessionUpdate({ + sessionId: params.sessionID, + update: { + sessionUpdate: "usage_update", + used: message.tokens.input + message.tokens.cache.read, + size, + cost: { amount: UsageService.totalSessionCost(messages), currency: "USD" }, + }, + }) + .catch((error) => { + log.error("failed to send usage update", { error }) + }), + ) + }) + + return UsageService.Service.of({ + buildUsage: UsageService.buildUsage, + latestAssistantMessage: UsageService.latestAssistantMessage, + totalSessionCost: UsageService.totalSessionCost, + contextLimit, + sendUpdate, + }) +} + +function replayMessages(subscription: ACPEvent.Subscription | undefined, messages: SessionMessageResponse[]) { + if (!subscription) return Effect.void + return Effect.promise(async () => { + for (const message of messages) { + await subscription.replayMessage(message).catch((error: unknown) => { + log.error("failed to replay ACP message", { error, messageID: message.info.id }) + }) + } + }) +} + +type ConfigState = { + readonly model: Directory.DefaultModel + readonly variant?: string + readonly modeId?: string +} + +type SdkResponse = { + readonly data?: T + readonly error?: unknown +} + +type MessageInfo = { + readonly role?: Message["role"] + readonly model?: Extract["model"] + readonly providerID?: Extract["providerID"] + readonly modelID?: Extract["modelID"] + readonly variant?: Extract["variant"] + readonly mode?: Extract["mode"] + readonly agent?: Message["agent"] +} + +type AssistantInfo = UsageService.AssistantTokenCost | undefined + +function request(fn: () => Promise>, service?: string) { + return Effect.tryPromise({ + try: async () => { + const result = await fn() + if (isSdkResponse(result)) { + if (result.error) throw result.error + if (result.data !== undefined) return result.data + } + return result as T + }, + catch: (error) => fromUnknownError(error, service), + }) +} + +function profiledRequest(name: string, fn: () => Promise>, service?: string) { + return request(() => ACPProfile.measure(name, fn), service) +} + +async function loadDirectorySnapshot(sdk: KiloClient, directory: string) { + return ACPProfile.measure("acp.directory.load", async () => { + const [providersResponse, agentsResponse, commandsResponse, skillsResponse, configResponse] = await Promise.all([ + ACPProfile.measure("acp.directory.provider.list", () => + sdk.config.providers({ directory }, { throwOnError: true }), + ), + ACPProfile.measure("acp.directory.mode.defaultAgent.load", () => + sdk.app.agents({ directory }, { throwOnError: true }), + ), + ACPProfile.measure("acp.directory.command.list", () => sdk.command.list({ directory }, { throwOnError: true })), + ACPProfile.measure("acp.directory.skill.list", () => sdk.app.skills({ directory }, { throwOnError: true })), + ACPProfile.measure("acp.directory.defaultModel.config", () => + sdk.config.get({ directory }, { throwOnError: true }).catch(() => undefined), + ), + ]) + const providersData = providersResponse.data! + const agents = agentsResponse.data! + const commandsData = commandsResponse.data! + const skills = skillsResponse.data! + const providers = Object.fromEntries(providersData.providers.map((provider) => [provider.id, provider])) as Record< + ProviderID, + Provider.Info + > + const defaultModelStarted = performance.now() + const defaultModel = defaultModelFromConfig(configResponse?.data?.model, providers) + ACPProfile.duration("acp.directory.defaultModel.resolve", defaultModelStarted, { configured: !!defaultModel }) + const modes = agents + .filter((agent) => agent.mode !== "subagent" && agent.hidden !== true) + .map((agent) => ({ + id: agent.name, + name: agent.name, + ...(agent.description ? { description: agent.description } : {}), + })) + const commands = [ + ...commandsData, + ...skills + .filter((skill) => !commandsData.some((command) => command.name === skill.name)) + .map((skill) => ({ + name: skill.name, + description: skill.description, + source: "skill" as const, + template: skill.content, + hints: [], + })), + ] as Command.Info[] + + return Directory.build({ + directory, + providers, + modes, + defaultModeID: agents.find((agent) => agent.mode === "primary" && agent.hidden !== true)?.name ?? "build", + commands: commands.toSorted((a, b) => a.name.localeCompare(b.name)), + ...(defaultModel ? { defaultModel } : {}), + }) + }) +} + +function defaultModelFromConfig( + configuredModel: string | undefined, + providers: Record, +): Directory.DefaultModel | undefined { + const configured = configuredModel ? Provider.parseModel(configuredModel) : undefined + if (configured && providers[configured.providerID]?.models[configured.modelID]) return configured + + // First-session ACP startup must not scan historical sessions just to infer + // a default. Configured model, opencode provider, then sorted best model keep + // the protocol response deterministic without extra session/message reads. + const kiloProvider = providers[ProviderID.kilo] // kilocode_change + const kiloModel = kiloProvider ? Provider.sort(Object.values(kiloProvider.models))[0] : undefined // kilocode_change + if (kiloProvider && kiloModel) return { providerID: kiloProvider.id, modelID: kiloModel.id } // kilocode_change + + const best = Provider.sort(Object.values(providers).flatMap((provider) => Object.values(provider.models)))[0] + if (best) return { providerID: best.providerID, modelID: best.id } + if (configured) return configured +} + +function selectDefaultModel(snapshot: Directory.Snapshot) { + if (snapshot.defaultModel) return snapshot.defaultModel + const model = snapshot.modelOptions[0] + if (model) return { providerID: model.providerID, modelID: model.modelID } + return { providerID: "unknown" as ProviderID, modelID: "unknown" as ModelID } +} + +function detectSlashCommand(parts: ReturnType) { + const text = parts + .filter((part): part is Extract<(typeof parts)[number], { type: "text" }> => part.type === "text") + .map((part) => part.text) + .join("") + .trim() + if (!text.startsWith("/")) return + + const [name, ...rest] = text.slice(1).split(/\s+/) + if (!name) return + return { name, args: rest.join(" ").trim() } +} + +function promptResponse(info: AssistantInfo, messageId: string | null | undefined): PromptResponse { + return { + stopReason: "end_turn", + ...(info ? { usage: UsageService.buildUsage(info) } : {}), + ...(messageId ? { userMessageId: messageId } : {}), + _meta: {}, + } +} + +function sendUsageUpdate( + usage: UsageService.Interface | undefined, + sdk: KiloClient, + connection: ServiceConnection | undefined, + sessionID: string, + directory: string, +) { + if (!connection) return Effect.void + return (usage ?? makeUsageService(sdk)).sendUpdate({ + connection, + sessionID, + directory, + }) +} + +function selectVariant(snapshot: Directory.Snapshot, model: Directory.DefaultModel) { + const variants = Directory.variants(snapshot, model) + if (!variants) return + if (variants.default) return "default" + return Object.keys(variants)[0] +} + +function configOptions(snapshot: Directory.Snapshot, session: ConfigState) { + return buildConfigOptions({ + providers: Object.values(snapshot.providers), + currentModel: session.model, + currentVariant: session.variant, + modes: snapshot.availableModes, + currentModeId: session.modeId, + }) +} + +function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) { + const selected = parseModelSelection(modelId, Object.values(snapshot.providers)) + const provider = snapshot.providers[ProviderID.make(selected.model.providerID)] + const model = provider?.models[ModelID.make(selected.model.modelID)] + if (!model) { + return Effect.fail( + new ACPError.InvalidModelError({ + providerId: selected.model.providerID, + modelId, + }), + ) + } + if (selected.variant && !model.variants?.[selected.variant]) { + return Effect.fail(new ACPError.InvalidEffortError({ effort: selected.variant })) + } + return Effect.succeed({ + model: { + providerID: provider.id, + modelID: model.id, + }, + variant: selected.variant, + }) +} + +function sendAvailableCommands( + connection: Pick | undefined, + sessionId: string, + snapshot: Directory.Snapshot, +) { + if (!connection) return Effect.void + return Effect.sync(() => { + setTimeout(() => { + void connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands: snapshot.availableCommands.map((command) => ({ + name: command.name, + description: command.description ?? "", + })), + }, + }) + }, 0) + }) +} + +function registerMcpServers( + sdk: KiloClient, + registered: Map>, + directory: string, + sessionId: string, + servers: readonly McpServer[], +) { + const started = performance.now() + const current = registered.get(sessionId) ?? new Set() + registered.set(sessionId, current) + const pending = new Set() + + return Effect.all( + servers + .map((server) => ({ server, config: mcpConfig(server) })) + .filter((entry) => { + const key = mcpRegistrationKey(entry.server.name, entry.config) + if (current.has(key) || pending.has(key)) return false + pending.add(key) + return true + }) + .map((entry) => + request( + () => + sdk.mcp.add( + { + directory, + name: entry.server.name, + config: entry.config, + }, + { throwOnError: true }, + ), + "mcp", + ).pipe( + Effect.tap(() => Effect.sync(() => current.add(mcpRegistrationKey(entry.server.name, entry.config)))), + Effect.ignore, + ), + ), + { concurrency: "unbounded" }, + ).pipe( + Effect.tap(() => + Effect.sync(() => + ACPProfile.duration("acp.mcp.register", started, { + count: pending.size, + }), + ), + ), + Effect.asVoid, + ) +} + +function mcpRegistrationKey(name: string, config: ReturnType) { + return `${name}:${stableStringify(config)}` +} + +function mcpConfig(server: McpServer) { + if ("type" in server) { + return { + type: "remote" as const, + url: server.url, + headers: Object.fromEntries(server.headers.map((header) => [header.name, header.value])), + } + } + return { + type: "local" as const, + command: [server.command, ...server.args], + environment: Object.fromEntries(server.env.map((entry) => [entry.name, entry.value])), + } +} + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]` + if (!value || typeof value !== "object") return JSON.stringify(value) + return `{${Object.entries(value) + .toSorted(([a], [b]) => a.localeCompare(b)) + .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`) + .join(",")}}` +} + +function restoreFromMessages(messages: readonly MessageInfo[]) { + const user = messages.findLast( + (message) => message.role === "user" && message.model?.providerID && message.model.modelID, + ) + if (user?.model?.providerID && user.model.modelID) { + return { + model: { providerID: user.model.providerID as ProviderID, modelID: user.model.modelID as ModelID }, + variant: user.model.variant, + modeId: user.agent, + } + } + + const assistant = messages.findLast((message) => message.providerID && message.modelID) + if (assistant?.providerID && assistant.modelID) { + return { + model: { providerID: assistant.providerID as ProviderID, modelID: assistant.modelID as ModelID }, + variant: assistant.variant, + modeId: assistant.mode ?? assistant.agent, + } + } + + return {} +} + +function isSdkResponse(value: T | SdkResponse): value is SdkResponse { + return typeof value === "object" && value !== null && ("data" in value || "error" in value) +} + +function fromUnknownError(error: unknown, service?: string): Error { + if (isACPError(error)) return error + if (isAuthRequired(error)) { + return new ACPError.AuthRequiredError({ providerId: findProviderID(error) }) + } + return new ACPError.ServiceFailureError({ safeMessage: "OpenCode service failure", service }) +} + +function isACPError(error: unknown): error is Error { + return ( + typeof error === "object" && + error !== null && + "_tag" in error && + typeof error._tag === "string" && + error._tag.startsWith("ACP") + ) +} + +function isAuthRequired(value: unknown): boolean { + if (typeof value !== "object" || value === null) return false + if (value instanceof Error && (value.name === "ProviderAuthError" || value.name === "LoadAPIKeyError")) return true + if ( + value instanceof Error && + (value.message.includes("ProviderAuthError") || value.message.includes("LoadAPIKeyError")) + ) { + return true + } + if ("name" in value && (value.name === "ProviderAuthError" || value.name === "LoadAPIKeyError")) return true + if ("_tag" in value && (value._tag === "ProviderAuthError" || value._tag === "LoadAPIKeyError")) return true + if ("error" in value && isAuthRequired(value.error)) return true + if ("data" in value && isAuthRequired(value.data)) return true + return false +} + +function findProviderID(value: unknown): string | undefined { + if (typeof value !== "object" || value === null) return + if ("providerID" in value && typeof value.providerID === "string") return value.providerID + if ("providerId" in value && typeof value.providerId === "string") return value.providerId + if ("data" in value) return findProviderID(value.data) + if ("error" in value) return findProviderID(value.error) +} diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 5ccbc4d84fd..4aced85949b 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -1,122 +1,230 @@ -import { RequestError, type McpServer } from "@agentclientprotocol/sdk" -import type { ACPSessionState } from "./types" -import * as Log from "@opencode-ai/core/util/log" -import type { KiloClient } from "@kilocode/sdk/v2" +import type { McpServer } from "@agentclientprotocol/sdk" +import type { Message, Part } from "@kilocode/sdk/v2" +import { Context, Effect, Layer, Ref } from "effect" +import type { ModelID, ProviderID } from "../provider/schema" +import * as ACPError from "./error" -const log = Log.create({ service: "acp-session-manager" }) +export type SelectedModel = { + providerID: ProviderID + modelID: ModelID +} -export class ACPSessionManager { - private sessions = new Map() - private sdk: KiloClient +export type KnownMessagePartMetadata = { + messageId: string + partId: string + partType?: Part["type"] + role?: Message["role"] + ignored?: boolean + toolCallId?: string + metadata?: unknown +} - constructor(sdk: KiloClient) { - this.sdk = sdk - } +export type Info = { + id: string + cwd: string + mcpServers: readonly McpServer[] + createdAt: Date + model?: SelectedModel + variant?: string + modeId?: string + knownParts: ReadonlyMap +} - tryGet(sessionId: string): ACPSessionState | undefined { - return this.sessions.get(sessionId) - } +export type StoreInput = { + id: string + cwd: string + mcpServers?: readonly McpServer[] + createdAt?: Date + model?: SelectedModel + variant?: string + modeId?: string +} - async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise { - const session = await this.sdk.session - .create( - { - directory: cwd, - }, - { throwOnError: true }, - ) - .then((x) => x.data!) +export type RecordPartMetadataInput = { + sessionId: string + messageId: string + partId: string + partType?: Part["type"] + role?: Message["role"] + ignored?: boolean + toolCallId?: string + metadata?: unknown +} - const sessionId = session.id - const resolvedModel = model +export type PartMetadataLookupInput = { + sessionId: string + messageId: string + partId: string +} - const state: ACPSessionState = { - id: sessionId, - cwd, - mcpServers, - createdAt: new Date(), - model: resolvedModel, - } - log.info("creating_session", { state }) - - this.sessions.set(sessionId, state) - return state - } - - async load( +export type Interface = { + readonly create: (input: StoreInput) => Effect.Effect + readonly load: (input: StoreInput) => Effect.Effect + readonly list: (cwd?: string) => Effect.Effect + readonly get: (sessionId: string) => Effect.Effect + readonly tryGet: (sessionId: string) => Effect.Effect + readonly remove: (sessionId: string) => Effect.Effect + readonly setModel: ( sessionId: string, - cwd: string, - mcpServers: McpServer[], - model?: ACPSessionState["model"], - ): Promise { - const session = await this.sdk.session - .get( - { - sessionID: sessionId, - directory: cwd, - }, - { throwOnError: true }, - ) - .then((x) => x.data!) + model: SelectedModel | undefined, + ) => Effect.Effect + readonly getModel: (sessionId: string) => Effect.Effect + readonly setVariant: ( + sessionId: string, + variant: string | undefined, + ) => Effect.Effect + readonly getVariant: (sessionId: string) => Effect.Effect + readonly setMode: ( + sessionId: string, + modeId: string | undefined, + ) => Effect.Effect + readonly getMode: (sessionId: string) => Effect.Effect + readonly recordPartMetadata: ( + input: RecordPartMetadataInput, + ) => Effect.Effect + readonly getPartMetadata: ( + input: PartMetadataLookupInput, + ) => Effect.Effect + readonly tryGetPartMetadata: (input: PartMetadataLookupInput) => Effect.Effect +} - const resolvedModel = model +export class Service extends Context.Service()("@opencode/ACP/Session") {} - const state: ACPSessionState = { - id: sessionId, - cwd, - mcpServers, - createdAt: new Date(session.time.created), - model: resolvedModel, - } - log.info("loading_session", { state }) +type State = Map - this.sessions.set(sessionId, state) - return state - } +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const sessions = yield* Ref.make(new Map()) - get(sessionId: string): ACPSessionState { - const session = this.sessions.get(sessionId) - if (!session) { - log.error("session not found", { sessionId }) - throw RequestError.invalidParams(JSON.stringify({ error: `Session not found: ${sessionId}` })) - } - return session - } + const store = Effect.fn("ACP.Session.store")(function* (input: StoreInput) { + const session = makeSession(input) + yield* Ref.update(sessions, (state) => new Map(state).set(session.id, session)) + return snapshot(session) + }) - getModel(sessionId: string) { - const session = this.get(sessionId) - return session.model - } + const tryGet = Effect.fn("ACP.Session.tryGet")(function* (sessionId: string) { + const session = (yield* Ref.get(sessions)).get(sessionId) + if (!session) return + return snapshot(session) + }) - setModel(sessionId: string, model: ACPSessionState["model"]) { - const session = this.get(sessionId) - session.model = model - this.sessions.set(sessionId, session) - return session - } + const get = Effect.fn("ACP.Session.get")(function* (sessionId: string) { + const session = yield* tryGet(sessionId) + if (session) return session + return yield* new ACPError.SessionNotFoundError({ sessionId }) + }) - getVariant(sessionId: string) { - const session = this.get(sessionId) - return session.variant - } + const update = Effect.fn("ACP.Session.update")(function* (sessionId: string, fn: (session: Info) => Info) { + const result = yield* Ref.modify(sessions, (state) => { + const session = state.get(sessionId) + if (!session) return [undefined, state] as const + const next = fn(session) + return [snapshot(next), new Map(state).set(sessionId, next)] as const + }) + if (result) return result + return yield* new ACPError.SessionNotFoundError({ sessionId }) + }) - setVariant(sessionId: string, variant?: string) { - const session = this.get(sessionId) - session.variant = variant - this.sessions.set(sessionId, session) - return session - } + const remove = Effect.fn("ACP.Session.remove")(function* (sessionId: string) { + return yield* Ref.modify(sessions, (state) => { + const session = state.get(sessionId) + if (!session) return [undefined, state] as const + const next = new Map(state) + next.delete(sessionId) + return [snapshot(session), next] as const + }) + }) - setMode(sessionId: string, modeId: string) { - const session = this.get(sessionId) - session.modeId = modeId - this.sessions.set(sessionId, session) - return session - } + const setModel: Interface["setModel"] = Effect.fn("ACP.Session.setModel")((sessionId, model) => + update(sessionId, (session) => ({ ...session, model })), + ) - remove(sessionId: string): ACPSessionState | undefined { - const session = this.sessions.get(sessionId) - this.sessions.delete(sessionId) - return session + const setVariant: Interface["setVariant"] = Effect.fn("ACP.Session.setVariant")((sessionId, variant) => + update(sessionId, (session) => ({ ...session, variant })), + ) + + const setMode: Interface["setMode"] = Effect.fn("ACP.Session.setMode")((sessionId, modeId) => + update(sessionId, (session) => ({ ...session, modeId })), + ) + + const recordPartMetadata: Interface["recordPartMetadata"] = Effect.fn("ACP.Session.recordPartMetadata")((input) => { + const metadata = { + messageId: input.messageId, + partId: input.partId, + partType: input.partType, + role: input.role, + ignored: input.ignored, + toolCallId: input.toolCallId, + metadata: input.metadata, + } + return update(input.sessionId, (session) => ({ + ...session, + knownParts: new Map(session.knownParts).set(partMetadataKey(input), metadata), + })).pipe(Effect.as(metadata)) + }) + + return Service.of({ + create: store, + load: store, + list: Effect.fn("ACP.Session.list")(function* (cwd?: string) { + return [...(yield* Ref.get(sessions)).values()] + .filter((session) => !cwd || session.cwd === cwd) + .map(snapshot) + .toSorted((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + }), + get, + tryGet, + remove, + setModel, + getModel: Effect.fn("ACP.Session.getModel")(function* (sessionId) { + return (yield* get(sessionId)).model + }), + setVariant, + getVariant: Effect.fn("ACP.Session.getVariant")(function* (sessionId) { + return (yield* get(sessionId)).variant + }), + setMode, + getMode: Effect.fn("ACP.Session.getMode")(function* (sessionId) { + return (yield* get(sessionId)).modeId + }), + recordPartMetadata, + getPartMetadata: Effect.fn("ACP.Session.getPartMetadata")(function* (input) { + return (yield* get(input.sessionId)).knownParts.get(partMetadataKey(input)) + }), + tryGetPartMetadata: Effect.fn("ACP.Session.tryGetPartMetadata")(function* (input) { + return (yield* tryGet(input.sessionId))?.knownParts.get(partMetadataKey(input)) + }), + }) + }), +) + +export const defaultLayer = layer + +function makeSession(input: StoreInput): Info { + return { + id: input.id, + cwd: input.cwd, + mcpServers: [...(input.mcpServers ?? [])], + createdAt: input.createdAt ? new Date(input.createdAt) : new Date(), + model: input.model, + variant: input.variant, + modeId: input.modeId, + knownParts: new Map(), } } + +function snapshot(session: Info): Info { + return { + ...session, + mcpServers: [...session.mcpServers], + createdAt: new Date(session.createdAt), + knownParts: new Map(session.knownParts), + } +} + +function partMetadataKey(input: { messageId: string; partId: string }) { + return `${input.messageId}:${input.partId}` +} + +export * as ACPSession from "./session" diff --git a/packages/opencode/src/acp/tool.ts b/packages/opencode/src/acp/tool.ts new file mode 100644 index 00000000000..08cf7ff845e --- /dev/null +++ b/packages/opencode/src/acp/tool.ts @@ -0,0 +1,290 @@ +import type { ToolCall, ToolCallContent, ToolCallLocation, ToolCallUpdate, ToolKind } from "@agentclientprotocol/sdk" + +export type ToolInput = Record + +export type ToolAttachment = { + readonly mime?: string + readonly url?: string + readonly [key: string]: unknown +} + +export type CompletedToolState = { + readonly status: "completed" + readonly input: ToolInput + readonly output: string + readonly metadata?: unknown + readonly attachments?: ReadonlyArray +} + +export type RunningToolState = { + readonly status: "running" + readonly input: ToolInput + readonly title?: string +} + +export type ErrorToolState = { + readonly status: "error" + readonly input: ToolInput + readonly error: string + readonly metadata?: unknown +} + +export type ImageAttachment = { + readonly mimeType: string + readonly data: string +} + +export function toToolKind(toolName: string): ToolKind { + const tool = toolName.toLocaleLowerCase() + + switch (tool) { + case "bash": + case "shell": + return "execute" + + case "webfetch": + return "fetch" + + case "edit": + case "patch": + case "write": + return "edit" + + case "grep": + case "glob": + case "repo_clone": + case "repo_overview": + case "context": + case "context7_resolve_library_id": + case "context7_get_library_docs": + return "search" + + case "read": + return "read" + + default: + return "other" + } +} + +export function toLocations(toolName: string, input: ToolInput): ToolCallLocation[] { + const tool = toolName.toLocaleLowerCase() + + switch (tool) { + case "read": + case "edit": + case "write": + return locationFrom(input.filePath ?? input.filepath) + + case "grep": + case "glob": + case "repo_clone": + case "repo_overview": + case "context": + case "context7_resolve_library_id": + case "context7_get_library_docs": + return locationFrom(input.path) + + case "bash": + case "shell": + return [] + + default: + return [] + } +} + +export function completedToolContent(toolName: string, state: CompletedToolState): ToolCallContent[] { + const content: ToolCallContent[] = [ + { + type: "content", + content: { + type: "text", + text: state.output, + }, + }, + ] + + if (toToolKind(toolName) === "edit") { + content.push(...diffContent(state.input)) + } + + content.push(...imageContents(state.attachments ?? [])) + return content +} + +export function pendingToolCall(input: { readonly toolCallId: string; readonly toolName: string }): ToolCall { + return { + toolCallId: input.toolCallId, + title: input.toolName, + kind: toToolKind(input.toolName), + status: "pending", + locations: [], + rawInput: {}, + } +} + +export function runningToolUpdate(input: { + readonly toolCallId: string + readonly toolName: string + readonly state: RunningToolState + readonly output?: string +}): ToolCallUpdate { + const content = input.output + ? [ + { + type: "content" as const, + content: { + type: "text" as const, + text: input.output, + }, + }, + ] + : undefined + + return { + toolCallId: input.toolCallId, + status: "in_progress", + kind: toToolKind(input.toolName), + title: input.state.title ?? input.toolName, + locations: toLocations(input.toolName, input.state.input), + rawInput: input.state.input, + ...(content ? { content } : {}), + } +} + +export function duplicateRunningToolUpdate(input: { + readonly toolCallId: string + readonly toolName: string + readonly state: RunningToolState +}): ToolCallUpdate { + return { + toolCallId: input.toolCallId, + status: "in_progress", + kind: toToolKind(input.toolName), + title: input.state.title ?? input.toolName, + locations: toLocations(input.toolName, input.state.input), + rawInput: input.state.input, + } +} + +export function completedToolUpdate(input: { + readonly toolCallId: string + readonly toolName: string + readonly state: CompletedToolState & { readonly title: string } +}): ToolCallUpdate { + return { + toolCallId: input.toolCallId, + status: "completed", + kind: toToolKind(input.toolName), + title: input.state.title, + content: completedToolContent(input.toolName, input.state), + rawInput: input.state.input, + rawOutput: completedToolRawOutput(input.state), + } +} + +export function errorToolUpdate(input: { + readonly toolCallId: string + readonly toolName: string + readonly state: ErrorToolState +}): ToolCallUpdate { + return { + toolCallId: input.toolCallId, + status: "failed", + kind: toToolKind(input.toolName), + title: input.toolName, + rawInput: input.state.input, + content: [ + { + type: "content", + content: { + type: "text", + text: input.state.error, + }, + }, + ], + rawOutput: { + error: input.state.error, + metadata: input.state.metadata, + }, + } +} + +export function completedToolRawOutput(state: CompletedToolState) { + return { + output: state.output, + ...(state.metadata !== undefined ? { metadata: state.metadata } : {}), + ...(state.attachments?.length ? { attachments: state.attachments } : {}), + } +} + +export function imageContents(attachments: ReadonlyArray): ToolCallContent[] { + return extractImageAttachments(attachments).map((attachment): ToolCallContent => { + return { + type: "content", + content: { + type: "image", + mimeType: attachment.mimeType, + data: attachment.data, + }, + } + }) +} + +export function extractImageAttachments(attachments: ReadonlyArray): ImageAttachment[] { + return attachments.flatMap((attachment): ImageAttachment[] => { + const data = dataUrlImage(attachment) + return data ? [data] : [] + }) +} + +export function shellOutputSnapshot(state: { readonly metadata?: unknown }) { + if (!state.metadata || typeof state.metadata !== "object") return undefined + return stringValue((state.metadata as Record).output) +} + +export const mapToolKind = toToolKind +export const extractLocations = toLocations +export const buildCompletedToolContent = completedToolContent +export const buildCompletedRawOutput = completedToolRawOutput +export const extractShellOutputSnapshot = shellOutputSnapshot +export const buildPendingToolCall = pendingToolCall +export const buildRunningToolUpdate = runningToolUpdate +export const buildDuplicateRunningToolUpdate = duplicateRunningToolUpdate +export const buildCompletedToolUpdate = completedToolUpdate +export const buildErrorToolUpdate = errorToolUpdate + +function locationFrom(value: unknown): ToolCallLocation[] { + const path = stringValue(value) + return path ? [{ path }] : [] +} + +function diffContent(input: ToolInput): ToolCallContent[] { + const oldText = stringValue(input.oldString) + const newText = stringValue(input.newString) ?? stringValue(input.content) + if (oldText === undefined || newText === undefined) return [] + + return [ + { + type: "diff", + path: stringValue(input.filePath) ?? "", + oldText, + newText, + }, + ] +} + +function dataUrlImage(attachment: ToolAttachment) { + const match = stringValue(attachment.url)?.match(/^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/) + const mime = match?.[1] ?? stringValue(attachment.mime) + if (!mime?.startsWith("image/")) return undefined + + const data = match?.[2] + if (data === undefined) return undefined + return { mimeType: mime, data } +} + +function stringValue(value: unknown) { + return typeof value === "string" ? value : undefined +} diff --git a/packages/opencode/src/acp/types.ts b/packages/opencode/src/acp/types.ts deleted file mode 100644 index a0d4fdbfcc9..00000000000 --- a/packages/opencode/src/acp/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { McpServer } from "@agentclientprotocol/sdk" -import type { KiloClient } from "@kilocode/sdk/v2" -import type { ProviderID, ModelID } from "../provider/schema" - -export interface ACPSessionState { - id: string - cwd: string - mcpServers: McpServer[] - createdAt: Date - model?: { - providerID: ProviderID - modelID: ModelID - } - variant?: string - modeId?: string -} - -export interface ACPConfig { - sdk: KiloClient - defaultModel?: { - providerID: ProviderID - modelID: ModelID - } -} diff --git a/packages/opencode/src/acp/usage.ts b/packages/opencode/src/acp/usage.ts new file mode 100644 index 00000000000..dcb21d5ceaf --- /dev/null +++ b/packages/opencode/src/acp/usage.ts @@ -0,0 +1,238 @@ +import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk" +import * as Log from "@opencode-ai/core/util/log" +import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@kilocode/sdk/v2" +import { InstanceRef } from "@/effect/instance-ref" +import { InstanceStore } from "@/project/instance-store" +import { ModelID, ProviderID } from "@/provider/schema" +import { Provider } from "@/provider/provider" +import { Context, Effect, Layer, SynchronizedRef } from "effect" + +const log = Log.create({ service: "acp-usage" }) + +export type AssistantTokenCost = Pick + +export type AssistantMessage = AssistantTokenCost & + Pick & + Partial> + +export type SessionMessage = { + readonly info: { readonly role: Message["role"] } | AssistantMessage +} + +export type MessagesInput = { + readonly sessionID: string + readonly directory: string +} + +export type SDK = { + readonly session: { + readonly messages: ( + parameters: { readonly sessionID: string; readonly directory: string }, + options: { readonly throwOnError: true }, + ) => Promise<{ readonly data?: readonly SessionMessage[] | null }> + } +} + +export interface MessageLoaderInterface { + readonly messages: (input: MessagesInput) => Effect.Effect +} + +export interface ContextLimitLoaderInterface { + readonly providers: (directory: string) => Effect.Effect, unknown> +} + +export type UsageConnection = Pick + +export interface Interface { + readonly buildUsage: (message: AssistantTokenCost) => Usage + readonly latestAssistantMessage: (messages: readonly SessionMessage[]) => AssistantMessage | undefined + readonly totalSessionCost: (messages: readonly SessionMessage[]) => number + readonly contextLimit: (input: { + readonly directory: string + readonly providerID: ProviderID + readonly modelID: ModelID + }) => Effect.Effect + readonly sendUpdate: (input: { + readonly connection: UsageConnection + readonly sessionID: string + readonly directory: string + }) => Effect.Effect +} + +export class MessageLoader extends Context.Service()( + "@opencode/ACPUsageMessageLoader", +) {} + +export class ContextLimitLoader extends Context.Service()( + "@opencode/ACPUsageContextLimitLoader", +) {} + +export class Service extends Context.Service()("@opencode/ACPUsage") {} + +export function messageLoaderFromSDK(sdk: SDK): MessageLoaderInterface { + return MessageLoader.of({ + messages: (input) => + Effect.promise(() => + sdk.session + .messages({ sessionID: input.sessionID, directory: input.directory }, { throwOnError: true }) + .then((response) => response.data ?? []), + ), + }) +} + +export const messageLoaderLayer = (sdk: SDK) => Layer.succeed(MessageLoader, messageLoaderFromSDK(sdk)) + +export function buildUsage(message: AssistantTokenCost): Usage { + const cachedReadTokens = message.tokens.cache.read + const cachedWriteTokens = message.tokens.cache.write + const thoughtTokens = message.tokens.reasoning + + return { + inputTokens: message.tokens.input, + outputTokens: message.tokens.output, + totalTokens: message.tokens.input + message.tokens.output + thoughtTokens + cachedReadTokens + cachedWriteTokens, + ...(thoughtTokens > 0 ? { thoughtTokens } : {}), + ...(cachedReadTokens > 0 ? { cachedReadTokens } : {}), + ...(cachedWriteTokens > 0 ? { cachedWriteTokens } : {}), + } +} + +export function latestAssistantMessage(messages: readonly SessionMessage[]): AssistantMessage | undefined { + return messages + .filter((message): message is { readonly info: AssistantMessage } => message.info.role === "assistant") + .at(-1)?.info +} + +export function totalSessionCost(messages: readonly SessionMessage[]): number { + return messages + .filter((message): message is { readonly info: AssistantMessage } => message.info.role === "assistant") + .reduce((sum, message) => sum + message.info.cost, 0) +} + +export function findContextLimit( + providers: Record, + providerID: ProviderID, + modelID: ModelID, +): number | undefined { + return providers[providerID]?.models[modelID]?.limit.context +} + +export const contextLimitLoaderLayer = Layer.effect( + ContextLimitLoader, + Effect.gen(function* () { + const store = yield* InstanceStore.Service + const provider = yield* Provider.Service + + return ContextLimitLoader.of({ + providers: Effect.fn("ACPUsageContextLimitLoader.providers")(function* (directory) { + const ctx = yield* store.load({ directory }) + return yield* Effect.gen(function* () { + return yield* provider.list() + }).pipe(Effect.provideService(InstanceRef, ctx)) + }), + }) + }), +) + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const messageLoader = yield* MessageLoader + const contextLimitLoader = yield* ContextLimitLoader + const limits = yield* SynchronizedRef.make(new Map>()) + + const cachedLimit = Effect.fnUntraced(function* (input: { + readonly directory: string + readonly providerID: ProviderID + readonly modelID: ModelID + }) { + return yield* SynchronizedRef.modifyEffect( + limits, + Effect.fnUntraced(function* (items) { + const key = `${input.directory}\u0000${input.providerID}\u0000${input.modelID}` + const current = items.get(key) + if (current) return [current, items] as const + const next = yield* Effect.cached( + contextLimitLoader.providers(input.directory).pipe( + Effect.map((providers) => findContextLimit(providers, input.providerID, input.modelID)), + Effect.catch((error) => + Effect.sync(() => { + log.error("failed to get providers for usage context limit", { error }) + return undefined + }), + ), + ), + ) + return [next, new Map(items).set(key, next)] as const + }), + ) + }) + + const contextLimit = Effect.fn("ACPUsage.contextLimit")(function* (input: { + readonly directory: string + readonly providerID: ProviderID + readonly modelID: ModelID + }) { + return yield* yield* cachedLimit(input) + }) + + const sendUpdate = Effect.fn("ACPUsage.sendUpdate")(function* (input: { + readonly connection: UsageConnection + readonly sessionID: string + readonly directory: string + }) { + const messages = yield* messageLoader.messages({ sessionID: input.sessionID, directory: input.directory }).pipe( + Effect.catch((error) => + Effect.sync(() => { + log.error("failed to fetch messages for usage update", { error }) + return undefined + }), + ), + ) + if (!messages) return + + const message = latestAssistantMessage(messages) + if (!message) return + if (!message.providerID || !message.modelID) return + + const size = yield* contextLimit({ + directory: input.directory, + providerID: ProviderID.make(message.providerID), + modelID: ModelID.make(message.modelID), + }) + if (!size) return + + yield* Effect.promise(() => + input.connection + .sessionUpdate({ + sessionId: input.sessionID, + update: { + sessionUpdate: "usage_update", + used: message.tokens.input + message.tokens.cache.read, + size, + cost: { amount: totalSessionCost(messages), currency: "USD" }, + }, + }) + .catch((error) => { + log.error("failed to send usage update", { error }) + }), + ) + }) + + return Service.of({ + buildUsage, + latestAssistantMessage, + totalSessionCost, + contextLimit, + sendUpdate, + }) + }), +) + +export const defaultLayer = layer.pipe( + Layer.provide(contextLimitLoaderLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(InstanceStore.defaultLayer), +) + +export * as UsageService from "./usage" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 4c8bcd4d37c..261f5ca83e6 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,4 +1,5 @@ import { Config } from "@/config/config" +import { serviceUse } from "@opencode-ai/core/effect/service-use" import { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" @@ -25,6 +26,7 @@ import { type DeepMutable } from "@opencode-ai/core/schema" import * as KiloAgent from "@/kilocode/agent" // kilocode_change import { RuntimeFlags } from "@/effect/runtime-flags" import { Reference } from "@/reference/reference" // kilocode_change +import { ConfigReference } from "@/config/reference" // kilocode_change export const Info = Schema.Struct({ name: Schema.String, @@ -71,7 +73,7 @@ export interface Interface { whenToUse: string systemPrompt: string }, - Provider.ModelNotFoundError + Provider.DefaultModelError > } @@ -79,6 +81,8 @@ type State = Omit & { version: string } // kilocode_chang export class Service extends Context.Service()("@opencode/Agent") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -365,7 +369,7 @@ export const layer = Layer.effect( if (flags.experimentalScout) { const resolvedReferences = Reference.resolveAll({ - references: cfg.reference ?? {}, + references: ConfigReference.normalize(cfg.reference ?? {}), // kilocode_change directory: ctx.directory, worktree: ctx.worktree, }) diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index ea3abd4ac11..bfe54cf2bd0 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -5,6 +5,7 @@ import { BusEvent } from "./bus-event" import { GlobalBus } from "./global" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { serviceUse } from "@opencode-ai/core/effect/service-use" import { Identifier } from "@/id/id" import { context as instanceContext, type InstanceContext } from "@/project/instance-context" // kilocode_change import { InstanceRef } from "@/effect/instance-ref" @@ -38,8 +39,16 @@ export interface Interface { properties: BusProperties, options?: { id?: string }, ) => Effect.Effect - readonly subscribe: (def: D) => Stream.Stream> - readonly subscribeAll: () => Stream.Stream + // subscribe / subscribeAll are eager: the underlying PubSub subscription is + // acquired in the caller's Scope at `yield*` time. Any publish after the + // yield is delivered, even if stream consumption starts later. The previous + // Stream-returning shape acquired the subscription lazily on first pull, + // opening a race window during which publishes were lost — see + // test/bus/bus-effect.test.ts RACE tests. + readonly subscribe: ( + def: D, + ) => Effect.Effect>, never, Scope.Scope> + readonly subscribeAll: () => Effect.Effect, never, Scope.Scope> readonly subscribeCallback: ( def: D, callback: (event: Payload) => unknown, @@ -49,6 +58,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Bus") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -110,26 +121,26 @@ export const layer = Layer.effect( }) } - function subscribe(def: D): Stream.Stream> { - log.info("subscribing", { type: def.type }) - return Stream.unwrap( - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - const ps = yield* getOrCreate(s, def) - return Stream.fromPubSub(ps) - }), - ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: def.type })))) - } + const subscribe = ( + def: D, + ): Effect.Effect>, never, Scope.Scope> => + Effect.gen(function* () { + log.info("subscribing", { type: def.type }) + const s = yield* InstanceState.get(state) + const ps = yield* getOrCreate(s, def) + const subscription = yield* PubSub.subscribe(ps) + yield* Effect.addFinalizer(() => Effect.sync(() => log.info("unsubscribing", { type: def.type }))) + return Stream.fromSubscription(subscription) + }) - function subscribeAll(): Stream.Stream { - log.info("subscribing", { type: "*" }) - return Stream.unwrap( - Effect.gen(function* () { - const s = yield* InstanceState.get(state) - return Stream.fromPubSub(s.wildcard) - }), - ).pipe(Stream.ensuring(Effect.sync(() => log.info("unsubscribing", { type: "*" })))) - } + const subscribeAll = (): Effect.Effect, never, Scope.Scope> => + Effect.gen(function* () { + log.info("subscribing", { type: "*" }) + const s = yield* InstanceState.get(state) + const subscription = yield* PubSub.subscribe(s.wildcard) + yield* Effect.addFinalizer(() => Effect.sync(() => log.info("unsubscribing", { type: "*" }))) + return Stream.fromSubscription(subscription) + }) function on(pubsub: PubSub.PubSub, type: string, callback: (event: T) => unknown) { return Effect.gen(function* () { diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index e0755577b61..b9cbf5569cf 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -15,6 +15,8 @@ const dim = (value: string) => UI.Style.TEXT_DIM + value + UI.Style.TEXT_NORMAL const activeSuffix = (isActive: boolean) => (isActive ? dim(" (active)") : "") +export const defaultConsoleUrl = "https://console.opencode.ai" + export const formatAccountLabel = (account: { email: string; url: string }, isActive: boolean) => `${account.email} ${dim(account.url)}${activeSuffix(isActive)}` @@ -173,18 +175,17 @@ const openEffect = Effect.fn("open")(function* () { }) export const LoginCommand = effectCmd({ - command: "login ", + command: "login [url]", describe: false, instance: false, builder: (yargs) => yargs.positional("url", { describe: "server URL", type: "string", - demandOption: true, }), handler: Effect.fn("Cli.account.login")(function* (args) { UI.empty() - yield* Effect.orDie(loginEffect(args.url)) + yield* Effect.orDie(loginEffect(args.url ?? defaultConsoleUrl)) }), }) diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index 8497417ca4c..4271df95d38 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -7,6 +7,7 @@ import { Server } from "@/server/server" import { ServerAuth } from "@/server/auth" import { createKiloClient } from "@kilocode/sdk/v2" import { withNetworkOptions, resolveNetworkOptions } from "../network" +import { ACPProfile } from "@/acp/profile" const log = Log.create({ service: "acp-command" }) @@ -21,9 +22,10 @@ export const AcpCommand = effectCmd({ }) }, handler: Effect.fn("Cli.acp")(function* (args) { + ACPProfile.mark("cli.acp.handler") process.env.KILO_CLIENT = "acp" const opts = yield* resolveNetworkOptions(args) - const server = yield* Effect.promise(() => Server.listen(opts)) + const server = yield* Effect.promise(() => ACPProfile.measure("cli.acp.server.listen", () => Server.listen(opts))) const sdk = createKiloClient({ baseUrl: `http://${server.hostname}:${server.port}`, @@ -57,7 +59,8 @@ export const AcpCommand = effectCmd({ const agent = ACP.init({ sdk }) new AgentSideConnection((conn) => { - return agent.create(conn, { sdk }) + ACPProfile.mark("cli.acp.connection.create") + return agent.create(conn) }, stream) log.info("setup connection") diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index ac9879ff891..c74c1c90794 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -1,6 +1,6 @@ import { EOL } from "os" import { basename } from "path" -import { Effect } from "effect" +import { Cause, Effect } from "effect" import { Agent } from "../../../agent/agent" import { Provider } from "@/provider/provider" import { Session } from "@/session/session" @@ -80,7 +80,21 @@ const run = Effect.fn("Cli.debug.agent.body")(function* ( const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) { const provider = yield* Provider.Service const registry = yield* ToolRegistry.Service - const model = agent.model ?? (yield* provider.defaultModel()) + const model = + agent.model ?? + (yield* provider.defaultModel().pipe( + Effect.matchCauseEffect({ + onSuccess: Effect.succeed, + onFailure: (cause) => { + const error = Cause.squash(cause) as Provider.DefaultModelError + if (error instanceof Provider.ModelNotFoundError) { + return fail(`Model not found: ${error.providerID}/${error.modelID}`) + } + if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`) + return fail("No providers found") + }, + }), + )) return yield* registry.tools({ ...model, agent }) }) @@ -133,7 +147,20 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio ? agent.model : yield* Effect.gen(function* () { const provider = yield* Provider.Service - return yield* provider.defaultModel() + return yield* provider.defaultModel().pipe( + Effect.matchCauseEffect({ + onSuccess: Effect.succeed, + onFailure: (cause) => { + const error = Cause.squash(cause) as Provider.DefaultModelError + if (error instanceof Provider.ModelNotFoundError) { + return fail(`Model not found: ${error.providerID}/${error.modelID}`) + } + if (error instanceof Provider.NoModelsError) + return fail(`No models found for provider ${error.providerID}`) + return fail("No providers found") + }, + }), + ) }) const now = Date.now() const message: MessageV2.Assistant = { diff --git a/packages/opencode/src/cli/cmd/debug/v2.ts b/packages/opencode/src/cli/cmd/debug/v2.ts index 836f5813672..aab7018982e 100644 --- a/packages/opencode/src/cli/cmd/debug/v2.ts +++ b/packages/opencode/src/cli/cmd/debug/v2.ts @@ -1,18 +1,17 @@ import { EOL } from "os" -import { Effect, Layer, Option } from "effect" +import { Effect, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { AbsolutePath } from "@opencode-ai/core/schema" import { effectCmd } from "../../effect-cmd" -const Runtime = Layer.mergeAll(LocationServiceMap.layer) - export const V2Command = effectCmd({ command: "v2", describe: "debug v2 catalog and built-in plugins", instance: false, - handler: Effect.fn("Cli.debug.v2")( - function* () { + handler: () => + Effect.gen(function* () { yield* PluginBoot.Service.use((service) => service.wait()) const catalog = yield* Catalog.Service const providers = (yield* catalog.provider.available()).sort((a, b) => a.id.localeCompare(b.id)) @@ -35,12 +34,13 @@ export const V2Command = effectCmd({ ), } process.stdout.write(JSON.stringify(result, null, 2) + EOL) - }, - Effect.provide( - LocationServiceMap.get({ - directory: process.cwd(), - }), + }).pipe( + Effect.withSpan("Cli.debug.v2"), + Effect.provide( + LocationServiceMap.get({ + directory: AbsolutePath.make(process.cwd()), + }), + ), + Effect.provide(LocationServiceMap.layer), ), - Effect.provide(Runtime), - ), }) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index c63b2e6d31e..722b321a0af 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -19,7 +19,7 @@ import type { import { UI } from "../ui" import { cmd } from "./cmd" import { effectCmd } from "../effect-cmd" -import { ModelsDev } from "@opencode-ai/core/models" +import { ModelsDev } from "@opencode-ai/core/models-dev" import { InstanceRef } from "@/effect/instance-ref" import { SessionShare } from "@/share/session" import { Session } from "@/session/session" diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 83de9efef51..62d7e29da16 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -6,9 +6,11 @@ import { Database } from "@/storage/db" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { InstanceRef } from "@/effect/instance-ref" import { EOL } from "os" +import path from "path" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Effect, Schema } from "effect" import * as Log from "@opencode-ai/core/util/log" // kilocode_change +import type { InstanceContext } from "@/project/instance-context" const log = Log.create({ service: "import" }) // kilocode_change @@ -131,11 +133,11 @@ export const ImportCommand = effectCmd({ handler: Effect.fn("Cli.import")(function* (args) { const ctx = yield* InstanceRef if (!ctx) return yield* Effect.die("InstanceRef not provided") - return yield* runImport(args.file, ctx.project.id) + return yield* runImport(args.file, ctx) }), }) -const runImport = Effect.fn("Cli.import.body")(function* (file: string, projectID: string) { +const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: InstanceContext) { const fs = yield* AppFileSystem.Service let exportData: ExportData | undefined @@ -198,14 +200,19 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, projectI const info = Schema.decodeUnknownSync(Session.Info)({ ...exportData.info, - projectID, + projectID: ctx.project.id, + directory: ctx.directory, + path: path.relative(path.resolve(ctx.worktree), ctx.directory).replaceAll("\\", "/"), }) as Session.Info const row = Session.toRow(info) Database.use((db) => db .insert(SessionTable) .values(row) - .onConflictDoUpdate({ target: SessionTable.id, set: { project_id: row.project_id } }) + .onConflictDoUpdate({ + target: SessionTable.id, + set: { project_id: row.project_id, directory: row.directory, path: row.path }, + }) .run(), ) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index c430920642e..a18a49de4bb 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -12,7 +12,6 @@ import { McpOAuthProvider } from "../../mcp/oauth-provider" import { Config } from "@/config/config" import { ConfigMCP } from "../../config/mcp" import { InstanceRef } from "@/effect/instance-ref" -import { Installation } from "../../installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" import path from "path" import { Global } from "@opencode-ai/core/global" diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index f4fa3e8bca2..c378a1006f9 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -2,7 +2,7 @@ import { EOL } from "os" import { Effect } from "effect" import { Provider } from "@/provider/provider" import { ProviderID } from "../../provider/schema" -import { ModelsDev } from "@opencode-ai/core/models" +import { ModelsDev } from "@opencode-ai/core/models-dev" import { effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 8983fe45028..d2d10d022df 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -3,7 +3,7 @@ import { cmd } from "./cmd" import { CliError, effectCmd, fail } from "../effect-cmd" import { UI } from "../ui" import * as Prompt from "../effect/prompt" -import { ModelsDev } from "@opencode-ai/core/models" +import { ModelsDev } from "@opencode-ai/core/models-dev" import { map, pipe, sortBy, values } from "remeda" import path from "path" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index cd2e23f7ddb..c056b6f67e7 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -23,7 +23,7 @@ import { ServerAuth } from "@/server/auth" import { buildRunMessage } from "@/kilocode/cli/cmd/run-message" // kilocode_change import { EOL } from "os" import { Filesystem } from "@/util/filesystem" -import { createKiloClient, type KiloClient, type ToolPart } from "@kilocode/sdk/v2" +import { createKiloClient, type KiloClient, type Session, type ToolPart } from "@kilocode/sdk/v2" import { Agent } from "@/agent/agent" import { Permission } from "@/permission" import { RuntimeFlags } from "@/effect/runtime-flags" @@ -33,7 +33,7 @@ import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime. import { event as normalizeEvent } from "./run/event" import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change -import { KiloRunDaemon } from "@/kilocode/cli/cmd/run" // kilocode_change +import { KiloRun, KiloRunDaemon } from "@/kilocode/cli/cmd/run" // kilocode_change const runtimeTask = import("./run/runtime") type ModelInput = Parameters[0]["model"] @@ -76,6 +76,7 @@ type SessionInfo = { id: string title?: string directory?: string + model?: Session["model"] } function inline(info: Inline) { @@ -231,6 +232,15 @@ export const RunCommand = effectCmd({ type: "boolean", describe: "show thinking blocks", }) + .option("replay", { + type: "boolean", + default: false, + describe: "replay visible session history on interactive resume", + }) + .option("replay-limit", { + type: "number", + describe: "cap visible interactive replay to the newest N messages", + }) .option("interactive", { alias: ["i"], type: "boolean", @@ -287,6 +297,21 @@ export const RunCommand = effectCmd({ die("--interactive cannot be used with --format json") } + if (args.replay && !args.interactive) { + die("--replay requires --interactive") + } + + if (args["replay-limit"] !== undefined && !args.interactive) { + die("--replay-limit requires --interactive") + } + + if ( + args["replay-limit"] !== undefined && + (!Number.isInteger(args["replay-limit"]) || args["replay-limit"] <= 0) + ) { + die("--replay-limit must be a positive integer") + } + if (args.interactive && !process.stdout.isTTY) { die("--interactive requires a TTY stdout") } @@ -299,6 +324,8 @@ export const RunCommand = effectCmd({ } } + const replay = args.replay || args["replay-limit"] !== undefined + const root = Filesystem.resolve(process.env.PWD ?? process.cwd()) const directory = (() => { if (!args.dir) return args.attach ? undefined : root @@ -345,14 +372,19 @@ export const RunCommand = effectCmd({ } } - const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() - message = resolveRunInput(message, piped) ?? "" - const initialInput = resolveRunInput(rawMessage, piped) - - if (message.trim().length === 0 && !args.command && !args.interactive) { + // kilocode_change start - defer stdin until endpoint-backed commands are classified + const input = { initial: undefined as string | undefined, loaded: false } + async function loadInput() { + if (input.loaded) return + const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() + message = resolveRunInput(message, piped) ?? "" + input.initial = resolveRunInput(rawMessage, piped) + input.loaded = true + if (message.trim().length > 0 || args.command || args.interactive) return UI.error("You must provide a message or a command") process.exit(1) } + // kilocode_change end if (args.fork && !args.continue && !args.session) { UI.error("--fork requires --continue or --session") @@ -422,6 +454,7 @@ export const RunCommand = effectCmd({ id: current.data.id, title: current.data.title, directory: current.data.directory, + model: current.data.model, } } // kilocode_change end @@ -451,6 +484,7 @@ export const RunCommand = effectCmd({ id, title: forked.data?.title ?? current.data.title, directory: forked.data?.directory ?? current.data.directory, + model: forked.data?.model ?? current.data.model, } } @@ -458,6 +492,7 @@ export const RunCommand = effectCmd({ id: current.data.id, title: current.data.title, directory: current.data.directory, + model: current.data.model, } } @@ -476,6 +511,7 @@ export const RunCommand = effectCmd({ id, title: forked.data?.title ?? base.title, directory: forked.data?.directory ?? base.directory, + model: forked.data?.model ?? base.model, } } @@ -484,13 +520,14 @@ export const RunCommand = effectCmd({ id: base.id, title: base.title, directory: base.directory, + model: base.model, } } const name = title() const result = await sdk.session.create({ title: name, - permission: rules, + permission: [...rules], }) const id = result.data?.id if (!id) { @@ -501,6 +538,7 @@ export const RunCommand = effectCmd({ id, title: result.data?.title ?? name, directory: result.data?.directory, + model: result.data?.model, } } @@ -533,7 +571,7 @@ export const RunCommand = effectCmd({ variant: input.variant, } : undefined, - permission: rules, + permission: [...rules], }) const id = result.data?.id if (!id) { @@ -640,6 +678,15 @@ export const RunCommand = effectCmd({ } async function execute(sdk: KiloClient) { + // kilocode_change start - preserve custom command precedence and avoid reading stdin for built-ins + const deferred = Boolean(args.attach && args.session && !directory) + const initial = deferred ? undefined : await KiloRun.resolveBuiltin(sdk, args.command, directory) + if (!deferred) { + KiloRun.validateBuiltin({ command: initial, continue: args.continue, session: args.session }) + if (!initial) await loadInput() + } + // kilocode_change end + const sess = await session(sdk) if (!sess?.id) { UI.error("Session not found") @@ -840,6 +887,13 @@ export const RunCommand = effectCmd({ } const cwd = args.attach ? (directory ?? sess.directory ?? (await current(sdk))) : (directory ?? root) const client = args.attach ? attachSDK(cwd) : sdk + // kilocode_change start - classify deferred attach commands in the session directory + const builtin = deferred ? await KiloRun.resolveBuiltin(client, args.command, cwd) : initial + if (deferred) { + KiloRun.validateBuiltin({ command: builtin, continue: args.continue, session: args.session }) + if (!builtin) await loadInput() + } + // kilocode_change end // Validate agent if specified const agent = await pickAgent(client) @@ -853,6 +907,17 @@ export const RunCommand = effectCmd({ process.exit(1) }) + // kilocode_change start - handle built-in session commands + if (builtin) { + const result = await KiloRun.runBuiltin(client, sessionID, builtin, args.model, sess.model, cwd) + if (result.error) { + if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error)) + process.exitCode = 1 + } + return + } + // kilocode_change end + if (args.command) { const result = await client.session.command({ sessionID, @@ -893,11 +958,13 @@ export const RunCommand = effectCmd({ sessionID, sessionTitle: sess.title, resume: Boolean(args.session || args.continue) && !args.fork, + replay, + replayLimit: args["replay-limit"], agent, model, variant: args.variant, files, - initialInput, + initialInput: input.initial, createSession: createFreshSession, thinking, demo: args.demo, @@ -909,6 +976,7 @@ export const RunCommand = effectCmd({ } if (args.interactive && !args.attach && !args.session && !args.continue) { + await loadInput() // kilocode_change - interactive local mode still consumes its initial input const model = pick(args.model) const { runInteractiveLocalMode } = await runtimeTask const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { @@ -928,8 +996,10 @@ export const RunCommand = effectCmd({ agent: args.agent, model, variant: args.variant, + replay, + replayLimit: args["replay-limit"], files, - initialInput, + initialInput: input.initial, thinking, demo: args.demo, }) diff --git a/packages/opencode/src/cli/cmd/run/footer.command.tsx b/packages/opencode/src/cli/cmd/run/footer.command.tsx index 4da370eabef..cf6822c0661 100644 --- a/packages/opencode/src/cli/cmd/run/footer.command.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.command.tsx @@ -6,7 +6,7 @@ import { createEffect, createMemo, createSignal, type Accessor } from "solid-js" import { RunFooterMenu, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu" import { formatBindings } from "./keymap.shared" import type { RunFooterTheme } from "./theme" -import type { FooterKeybinds, RunCommand, RunInput, RunProvider } from "./types" +import type { FooterKeybinds, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types" type PanelEntry = RunFooterMenuItem & { category: string @@ -15,6 +15,7 @@ type PanelEntry = RunFooterMenuItem & { type CommandEntry = | (PanelEntry & { action: "model" }) + | (PanelEntry & { action: "subagent" }) | (PanelEntry & { action: "variant.cycle" }) | (PanelEntry & { action: "variant.list" }) | (PanelEntry & { action: "slash"; name: string }) @@ -32,11 +33,19 @@ type VariantEntry = PanelEntry & { current: boolean } +type SubagentEntry = PanelEntry & { + sessionID: string + current: boolean +} + type MenuState = ReturnType const PANEL_PAD = 2 const PANEL_LIST_ROWS = 10 -export const RUN_COMMAND_PANEL_ROWS = PANEL_LIST_ROWS + 6 +const PANEL_FRAME_ROWS = 6 +export const RUN_COMMAND_PANEL_ROWS = PANEL_LIST_ROWS + PANEL_FRAME_ROWS +const SUBAGENT_LIST_ROWS = 12 +export const RUN_SUBAGENT_PANEL_ROWS = SUBAGENT_LIST_ROWS + PANEL_FRAME_ROWS const PANEL_PAGE = PANEL_LIST_ROWS - 1 const PANEL_BORDER = { topLeft: "", @@ -89,6 +98,18 @@ function categoryRank(category: string) { return 2 } +function subagentStatusLabel(status: FooterSubagentTab["status"]) { + if (status === "completed") { + return "done" + } + + if (status === "error") { + return "error" + } + + return "running" +} + function handleKey(input: { event: KeyEvent menu: MenuState @@ -273,10 +294,12 @@ function PanelShell(props: { export function RunCommandMenuBody(props: { theme: Accessor commands: Accessor + subagents: Accessor variants: Accessor keybinds: FooterKeybinds onClose: () => void onModel: () => void + onSubagent: () => void onVariant: () => void onVariantCycle: () => void onCommand: (name: string) => void @@ -293,6 +316,20 @@ export function RunCommandMenuBody(props: { category: "Suggested", display: "Switch model", }, + ...(props.subagents().length > 0 + ? [ + { + action: "subagent" as const, + category: "Suggested", + display: "View subagents", + footer: `${props.subagents().length} active`, + keywords: props + .subagents() + .map((item) => `${item.label} ${item.description} ${item.title ?? ""}`) + .join(" "), + }, + ] + : []), { action: "variant.cycle", category: "Suggested", @@ -346,6 +383,11 @@ export function RunCommandMenuBody(props: { return } + if (item.action === "subagent") { + props.onSubagent() + return + } + if (item.action === "variant.cycle") { props.onVariantCycle() return @@ -423,6 +465,101 @@ export function RunCommandMenuBody(props: { ) } +export function RunSubagentSelectBody(props: { + theme: Accessor + tabs: Accessor + current: Accessor + onClose: () => void + onSelect: (sessionID: string) => void + onRows?: (rows: number) => void +}) { + let field: InputRenderable | undefined + const [query, setQuery] = createSignal("") + const entries = createMemo(() => + props.tabs().map((item) => { + const title = item.description || item.title || item.label + return { + category: "", + display: title, + description: title === item.label ? undefined : item.label, + footer: subagentStatusLabel(item.status), + keywords: `${item.label} ${item.description} ${item.title ?? ""} ${item.status}`, + sessionID: item.sessionID, + current: props.current() === item.sessionID, + } + }), + ) + const items = createMemo(() => match(query(), entries())) + const menu = createFooterMenuState({ count: () => items().length, limit: SUBAGENT_LIST_ROWS }) + const select = () => { + const item = items()[menu.selected()] + if (!item) { + return + } + + props.onSelect(item.sessionID) + } + + createEffect(() => { + query() + menu.reset() + }) + + createEffect(() => { + if (query().trim()) { + return + } + + const index = items().findIndex((item) => item.current) + if (index !== -1) { + menu.reveal(index) + } + }) + + createEffect(() => { + props.onRows?.(menu.rows() + PANEL_FRAME_ROWS) + }) + + useKeyboard((event) => { + if (event.defaultPrevented) { + return + } + + handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose }) + }) + + return ( + { + field = input + }} + onQuery={setQuery} + > + + + ) +} + export function RunVariantSelectBody(props: { theme: Accessor variants: Accessor diff --git a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx index fcde863d014..f8e3cf98f35 100644 --- a/packages/opencode/src/cli/cmd/run/footer.prompt.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.prompt.tsx @@ -7,7 +7,7 @@ /** @jsxImportSource @opentui/solid */ import { pathToFileURL } from "bun" import { StyledText, bg, fg, type KeyBinding, type KeyEvent, type TextareaRenderable } from "@opentui/core" -import { useKeyboard } from "@opentui/solid" +import { useKeyboard, useRenderer } from "@opentui/solid" import fuzzysort from "fuzzysort" import path from "path" import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js" @@ -67,6 +67,7 @@ type PromptInput = { directory: string findFiles: (query: string) => Promise agents: Accessor + subagents: Accessor resources: Accessor commands: Accessor keybinds: FooterKeybinds @@ -82,6 +83,7 @@ type PromptInput = { onInputClear: () => void onExitRequest?: () => boolean onExit: () => void + onSubagentMenu?: () => void onRows: (rows: number) => void onStatus: (text: string) => void } @@ -89,6 +91,7 @@ type PromptInput = { export type PromptState = { placeholder: Accessor bindings: Accessor + shell: Accessor visible: Accessor options: Accessor selected: Accessor @@ -111,9 +114,14 @@ function clonePrompt(prompt: RunPrompt): RunPrompt { return { text: prompt.text, parts: structuredClone(prompt.parts), + ...(prompt.mode ? { mode: prompt.mode } : {}), } } +function emptyPrompt(shell: boolean): RunPrompt { + return shell ? { text: "", parts: [], mode: "shell" } : { text: "", parts: [] } +} + function removeLineRange(input: string) { const hash = input.lastIndexOf("#") return hash === -1 ? input : input.slice(0, hash) @@ -199,13 +207,45 @@ export function RunPromptBody(props: { onContentChange: () => void bind: (area?: TextareaRenderable) => void }) { + const renderer = useRenderer() let area: TextareaRenderable | undefined + let pasteTick: ReturnType | undefined + + const refreshPasteLayout = () => { + if (pasteTick) { + clearTimeout(pasteTick) + } + + pasteTick = setTimeout(() => { + pasteTick = undefined + if (!area || area.isDestroyed) { + return + } + + // Paste can leave the textarea layout stale until the next edit. + area.getLayoutNode().markDirty() + renderer.requestRender() + void renderer + .idle() + .then(() => { + if (!area || area.isDestroyed) { + return + } + + props.onContentChange() + }) + .catch(() => {}) + }, 0) + } onMount(() => { props.bind(area) }) onCleanup(() => { + if (pasteTick) { + clearTimeout(pasteTick) + } props.bind(undefined) }) @@ -228,6 +268,9 @@ export function RunPromptBody(props: { keyBindings={props.bindings()} onSubmit={props.onSubmit} onKeyDown={props.onKeyDown} + onPaste={() => { + refreshPasteLayout() + }} onContentChange={props.onContentChange} ref={(next) => { area = next @@ -241,7 +284,12 @@ export function RunPromptBody(props: { export function createPromptState(input: PromptInput): PromptState { const keys = createMemo(() => promptKeys(input.keybinds)) const bindings = createMemo(() => keys().bindings) + const [shell, setShell] = createSignal(false) const placeholder = createMemo(() => { + if (shell()) { + return new StyledText([bg(input.theme().surface)(fg(input.theme().muted)('Run a command... "git status"'))]) + } + if (!input.state().first) { return "" } @@ -268,6 +316,11 @@ export function createPromptState(input: PromptInput): PromptState { const [query, setQuery] = createSignal("") const visible = createMemo(() => mode() !== false) + const setShellMode = (value: boolean) => { + setShell(value) + draft = value ? { ...draft, mode: "shell" } : { text: draft.text, parts: structuredClone(draft.parts) } + } + const width = createMemo(() => Math.max(20, input.width() - 8)) const agents = createMemo(() => { return input @@ -544,6 +597,7 @@ export function createPromptState(input: PromptInput): PromptState { const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => { draft = clonePrompt(value) + setShell(value.mode === "shell") if (!area || area.isDestroyed) { return } @@ -563,7 +617,7 @@ export function createPromptState(input: PromptInput): PromptState { clearParts() hide() - draft = { text: "", parts: [] } + draft = emptyPrompt(shell()) if (!area || area.isDestroyed) { return } @@ -573,7 +627,7 @@ export function createPromptState(input: PromptInput): PromptState { } const replaceDraft = (text: string) => { - draft = { text, parts: [] } + draft = shell() ? { text, parts: [], mode: "shell" } : { text, parts: [] } if (!area || area.isDestroyed) { return } @@ -581,7 +635,7 @@ export function createPromptState(input: PromptInput): PromptState { hide() area.setText(text) clearParts() - draft = { text: area.plainText, parts: [] } + draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] } area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText)) scheduleRows() area.focus() @@ -672,10 +726,16 @@ export function createPromptState(input: PromptInput): PromptState { } syncParts() - draft = { - text: area.plainText, - parts: structuredClone(parts), - } + draft = shell() + ? { + text: area.plainText, + parts: structuredClone(parts), + mode: "shell", + } + : { + text: area.plainText, + parts: structuredClone(parts), + } } const push = (value: RunPrompt) => { @@ -910,6 +970,52 @@ export function createPromptState(input: PromptInput): PromptState { } } + if ( + key.name === "!" && + !shell() && + !event.ctrl && + !event.meta && + !event.super && + area && + !area.isDestroyed && + area.cursorOffset === 0 + ) { + event.preventDefault() + setShellMode(true) + return + } + + if (shell() && !visible()) { + if (key.name === "escape") { + event.preventDefault() + setShellMode(false) + return + } + + if (key.name === "backspace" && area && !area.isDestroyed && area.cursorOffset === 0) { + event.preventDefault() + setShellMode(false) + return + } + } + + if ( + key.name === "down" && + !visible() && + !event.ctrl && + !event.meta && + !event.shift && + !event.super && + area && + !area.isDestroyed && + area.plainText.length === 0 && + input.subagents() > 0 + ) { + event.preventDefault() + input.onSubagentMenu?.() + return + } + if (promptHit(keys().clear, key)) { const handled = requestExit() if (handled) { @@ -964,7 +1070,12 @@ export function createPromptState(input: PromptInput): PromptState { return } - if (input.view() === "command" || input.view() === "model" || input.view() === "variant") { + if ( + input.view() === "command" || + input.view() === "model" || + input.view() === "variant" || + input.view() === "subagent-menu" + ) { return } @@ -995,23 +1106,29 @@ export function createPromptState(input: PromptInput): PromptState { return } - if (isExitCommand(next.text)) { + if (next.mode !== "shell" && isExitCommand(next.text)) { input.onExit() return } - const parsed = isNewCommand(next.text) ? undefined : parseSlashCommand(next.text, input.commands()) + const parsed = + next.mode === "shell" || isNewCommand(next.text) ? undefined : parseSlashCommand(next.text, input.commands()) if (parsed?.type === "pending") { input.onStatus("loading commands") return } const submit = parsed?.type === "command" ? { ...next, command: parsed.command } : next + const shellMode = next.mode === "shell" resetDraft() queueMicrotask(async () => { if (await input.onSubmit(submit)) { push(next) + if (shellMode) { + setShellMode(false) + draft = emptyPrompt(false) + } return } @@ -1088,6 +1205,7 @@ export function createPromptState(input: PromptInput): PromptState { return { placeholder, bindings, + shell, visible, options, selected: menu.selected, diff --git a/packages/opencode/src/cli/cmd/run/footer.question.tsx b/packages/opencode/src/cli/cmd/run/footer.question.tsx index 4a0d1f36264..bafb2c4676b 100644 --- a/packages/opencode/src/cli/cmd/run/footer.question.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.question.tsx @@ -416,7 +416,7 @@ export function RunQuestionBody(props: { - {hit() ? "✓" : ""} + {hit() ? " ✓" : ""} @@ -466,7 +466,7 @@ export function RunQuestionBody(props: { - {picked() ? "✓" : ""} + {picked() ? " ✓" : ""} = 8 || perTab < 12) { - return `[${slot}]` - } - - const prefix = `[${slot}]` - if (count >= 5 || perTab < 24) { - return prefix - } - - const label = tab.description || tab.title || tab.label - return `${prefix} ${label}` -} - -export function RunFooterSubagentTabs(props: { - tabs: FooterSubagentTab[] - selected?: string - theme: RunFooterTheme - width: number -}) { - const items = mapArray( - () => props.tabs, - (tab, index) => { - const active = () => props.selected === tab.sessionID - const slot = () => String(index() + 1) - return ( - - - {tab.status === "running" ? ( - - - - ) : ( - - {statusIcon(tab.status)} - - )} - - {tabText(tab, slot(), props.tabs.length, props.width)} - - - - ) - }, - ) - - return ( - - - {items()} - - - ) -} - export function RunFooterSubagentBody(props: { active: () => boolean theme: () => RunTheme + tab: () => FooterSubagentTab | undefined + index: () => number + total: () => number detail: () => FooterSubagentDetail | undefined width: () => number diffStyle?: RunDiffStyle @@ -111,6 +48,7 @@ export function RunFooterSubagentBody(props: { }) { const theme = createMemo(() => props.theme()) const footer = createMemo(() => theme().footer) + const tab = createMemo(() => props.tab()) const commits = createMemo(() => props.detail()?.commits ?? []) const opts = createMemo(() => ({ diffStyle: props.diffStyle })) const scrollbar = createMemo(() => ({ @@ -119,6 +57,22 @@ export function RunFooterSubagentBody(props: { foregroundColor: footer().line, }, })) + const title = createMemo(() => { + const current = tab() + if (!current) { + return "" + } + + return current.description || current.title || current.label + }) + const subtitle = createMemo(() => { + const current = tab() + if (!current || title() === current.label) { + return "" + } + + return current.label + }) const rows = indexArray(commits, (commit, index) => ( {index > 0 && separatorRows(commits()[index - 1], commit()) > 0 ? : null} @@ -165,6 +119,32 @@ export function RunFooterSubagentBody(props: { backgroundColor={footer().surface} > + + {(current) => ( + + {current().status === "running" ? ( + + + + ) : ( + + {statusIcon(current().status)} + + )} + + {title()} + 0}> + {" " + subtitle()} + + + 1 && props.index() > 0}> + + {props.index()} of {props.total()} + + + + )} + private setSubagent: (next: FooterSubagentState) => void private promptRoute: FooterPromptRoute = { type: "composer" } - private tabsVisible = false + private subagentMenuRows = SUBAGENT_ROWS private autocomplete = false private interruptTimeout: NodeJS.Timeout | undefined private exitTimeout: NodeJS.Timeout | undefined @@ -553,23 +554,24 @@ export class RunFooter implements FooterApi { // get fixed extra rows; the prompt view scales with textarea line count. private applyHeight(): void { const type = this.view().type - const tabs = this.tabsVisible ? SUBAGENT_TAB_ROWS : 0 const compact = this.promptRoute.type === "composer" && this.autocomplete ? AUTOCOMPLETE_COMPACT_ROWS : 0 - const base = this.base + tabs - compact + const base = this.base - compact const height = type === "permission" ? this.base + PERMISSION_ROWS : type === "question" ? this.base + QUESTION_ROWS : this.promptRoute.type === "command" - ? 1 + tabs + COMMAND_ROWS + ? 1 + COMMAND_ROWS : this.promptRoute.type === "model" - ? 1 + tabs + MODEL_ROWS + ? 1 + MODEL_ROWS : this.promptRoute.type === "variant" - ? 1 + tabs + VARIANT_ROWS - : this.promptRoute.type === "subagent" - ? this.base + tabs + SUBAGENT_INSPECTOR_ROWS - : Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows)) + ? 1 + VARIANT_ROWS + : this.promptRoute.type === "subagent-menu" + ? 1 + this.subagentMenuRows + : this.promptRoute.type === "subagent" + ? this.base + SUBAGENT_INSPECTOR_ROWS + : Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows)) if (height !== this.renderer.footerHeight) { this.renderer.footerHeight = height @@ -592,10 +594,10 @@ export class RunFooter implements FooterApi { } } - private syncLayout = (next: { route: FooterPromptRoute; tabs: boolean; autocomplete: boolean }): void => { + private syncLayout = (next: { route: FooterPromptRoute; autocomplete: boolean; subagentRows: number }): void => { this.promptRoute = next.route - this.tabsVisible = next.tabs this.autocomplete = next.autocomplete + this.subagentMenuRows = next.subagentRows if (this.view().type === "prompt") { this.applyHeight() } diff --git a/packages/opencode/src/cli/cmd/run/footer.view.tsx b/packages/opencode/src/cli/cmd/run/footer.view.tsx index e1a028b7e95..affc664b155 100644 --- a/packages/opencode/src/cli/cmd/run/footer.view.tsx +++ b/packages/opencode/src/cli/cmd/run/footer.view.tsx @@ -14,9 +14,15 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import "opentui-spinner/solid" import { createColors, createFrames } from "../tui/ui/spinner" -import { RunCommandMenuBody, RunModelSelectBody, RunVariantSelectBody } from "./footer.command" +import { + RUN_SUBAGENT_PANEL_ROWS, + RunCommandMenuBody, + RunModelSelectBody, + RunSubagentSelectBody, + RunVariantSelectBody, +} from "./footer.command" import { FOOTER_MENU_ROWS, RunFooterMenu } from "./footer.menu" -import { RunFooterSubagentBody, RunFooterSubagentTabs } from "./footer.subagent" +import { RunFooterSubagentBody } from "./footer.subagent" import { RunPromptBody, createPromptState, hintFlags } from "./footer.prompt" import { RunPermissionBody } from "./footer.permission" import { RunQuestionBody } from "./footer.question" @@ -85,30 +91,11 @@ type RunFooterViewProps = { onModelSelect: (model: NonNullable) => void onVariantSelect: (variant: string | undefined) => void onRows: (rows: number) => void - onLayout: (input: { route: FooterPromptRoute; tabs: boolean; autocomplete: boolean }) => void + onLayout: (input: { route: FooterPromptRoute; autocomplete: boolean; subagentRows: number }) => void onStatus: (text: string) => void onSubagentSelect?: (sessionID: string | undefined) => void } -function subagentShortcut(event: { - name: string - ctrl?: boolean - meta?: boolean - shift?: boolean - super?: boolean -}): number | undefined { - if (!event.ctrl || event.meta || event.super) { - return undefined - } - - if (!/^[0-9]$/.test(event.name)) { - return undefined - } - - const slot = Number(event.name) - return slot === 0 ? 9 : slot - 1 -} - export { TEXTAREA_MIN_ROWS, TEXTAREA_MAX_ROWS } from "./footer.prompt" export function RunFooterView(props: RunFooterViewProps) { @@ -125,18 +112,39 @@ export function RunFooterView(props: RunFooterViewProps) { ) }) const [route, setRoute] = createSignal({ type: "composer" }) + const [subagentMenuRows, setSubagentMenuRows] = createSignal(RUN_SUBAGENT_PANEL_ROWS) const prompt = createMemo(() => active().type === "prompt" && route().type === "composer") + const selectingSubagent = createMemo(() => active().type === "prompt" && route().type === "subagent-menu") const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent") const commanding = createMemo(() => active().type === "prompt" && route().type === "command") const modeling = createMemo(() => active().type === "prompt" && route().type === "model") const varianting = createMemo(() => active().type === "prompt" && route().type === "variant") - const panel = createMemo(() => commanding() || modeling() || varianting()) + const panel = createMemo(() => selectingSubagent() || commanding() || modeling() || varianting()) const selected = createMemo(() => { const current = route() return current.type === "subagent" ? current.sessionID : undefined }) const tabs = createMemo(() => subagent().tabs) - const showTabs = createMemo(() => active().type === "prompt" && tabs().length > 0) + const selectedTab = createMemo(() => tabs().find((item) => item.sessionID === selected())) + const selectedIndex = createMemo(() => { + const sessionID = selected() + if (!sessionID) { + return 0 + } + + return tabs().findIndex((item) => item.sessionID === sessionID) + 1 + }) + const subagentIndicator = createMemo(() => { + const count = tabs().length + if (count === 0) { + return + } + + return { + count, + label: count === 1 ? "agent" : "agents", + } + }) const detail = createMemo(() => { const current = route() return current.type === "subagent" ? subagent().details[current.sessionID] : undefined @@ -203,6 +211,15 @@ export function RunFooterView(props: RunFooterViewProps) { props.onSubagentSelect?.(undefined) } + const openSubagentMenu = () => { + if (tabs().length === 0) { + return + } + + setRoute({ type: "subagent-menu" }) + props.onSubagentSelect?.(undefined) + } + const closePanel = () => { setRoute({ type: "composer" }) } @@ -217,16 +234,6 @@ export function RunFooterView(props: RunFooterViewProps) { props.onSubagentSelect?.(undefined) } - const toggleTab = (sessionID: string) => { - const current = route() - if (current.type === "subagent" && current.sessionID === sessionID) { - closeTab() - return - } - - openTab(sessionID) - } - const cycleTab = (dir: -1 | 1) => { if (tabs().length === 0) { return @@ -247,6 +254,7 @@ export function RunFooterView(props: RunFooterViewProps) { directory: props.directory, findFiles: props.findFiles, agents: props.agents, + subagents: () => tabs().length, resources: props.resources, commands: props.commands, keybinds: props.keybinds, @@ -262,9 +270,11 @@ export function RunFooterView(props: RunFooterViewProps) { onInputClear: props.onInputClear, onExitRequest: props.onExitRequest, onExit: props.onExit, + onSubagentMenu: openSubagentMenu, onRows: props.onRows, onStatus: props.onStatus, }) + const shell = createMemo(() => prompt() && composer.shell()) const menu = createMemo(() => prompt() && composer.visible()) createEffect(() => { @@ -300,23 +310,6 @@ export function RunFooterView(props: RunFooterViewProps) { openCommand() }) - useKeyboard((event) => { - if (active().type !== "prompt") { - return - } - - const slot = subagentShortcut(event) - if (slot !== undefined) { - const next = tabs()[slot] - if (!next) { - return - } - - event.preventDefault() - toggleTab(next.sessionID) - } - }) - createEffect(() => { const current = route() if (current.type !== "subagent") { @@ -330,13 +323,30 @@ export function RunFooterView(props: RunFooterViewProps) { closeTab() }) + createEffect(() => { + if (route().type !== "subagent-menu") { + return + } + + if (tabs().length > 0) { + return + } + + closePanel() + }) + createEffect(() => { if (active().type === "prompt") { return } const current = route() - if (current.type !== "command" && current.type !== "model" && current.type !== "variant") { + if ( + current.type !== "command" && + current.type !== "model" && + current.type !== "variant" && + current.type !== "subagent-menu" + ) { return } @@ -346,8 +356,8 @@ export function RunFooterView(props: RunFooterViewProps) { createEffect(() => { props.onLayout({ route: route(), - tabs: tabs().length > 0, autocomplete: menu(), + subagentRows: subagentMenuRows(), }) }) @@ -364,10 +374,6 @@ export function RunFooterView(props: RunFooterViewProps) { > - - - - + + + { props.onCycle() @@ -487,18 +505,20 @@ export function RunFooterView(props: RunFooterViewProps) { paddingTop={1} > - {props.agent} - - - {props.state().model} + {shell() ? "Shell" : props.agent} + + + {props.state().model} + + @@ -570,22 +590,16 @@ export function RunFooterView(props: RunFooterViewProps) { gap={1} flexShrink={0} > - - + 0 || subagentIndicator()}> + - + Press Ctrl-c again to exit - + @@ -601,22 +615,36 @@ export function RunFooterView(props: RunFooterViewProps) { - - - 0}> - - - ▣ - - - - · - - - {duration()} - - + 0}> + + + ▣ + + + + · + + + {duration()} + + + + + + + {(info) => ( + + 0}> + · + + {info().count} {info().label} + · + + to view + + )} + @@ -629,19 +657,30 @@ export function RunFooterView(props: RunFooterViewProps) { flexShrink={0} justifyContent="flex-end" > - 0}> - - {queue()} queued - - - 0}> - - {usage()} - - - 0 && hints().command}> - - {command()} commands + + 0}> + + {queue()} queued + + + 0}> + + {usage()} + + + 0 && hints().command}> + + {command()} commands + + + + } + > + + esc exit shell mode @@ -706,6 +745,9 @@ export function RunFooterView(props: RunFooterViewProps) { tabs().length} detail={detail} width={() => term().width} diffStyle={props.diffStyle} diff --git a/packages/opencode/src/cli/cmd/run/prompt.shared.ts b/packages/opencode/src/cli/cmd/run/prompt.shared.ts index 0da787cb3c8..2dda26bae10 100644 --- a/packages/opencode/src/cli/cmd/run/prompt.shared.ts +++ b/packages/opencode/src/cli/cmd/run/prompt.shared.ts @@ -65,11 +65,12 @@ export function promptCopy(prompt: RunPrompt): RunPrompt { return { text: prompt.text, parts: structuredClone(prompt.parts), + ...(prompt.mode ? { mode: prompt.mode } : {}), } } export function promptSame(a: RunPrompt, b: RunPrompt): boolean { - return a.text === b.text && JSON.stringify(a.parts) === JSON.stringify(b.parts) + return a.mode === b.mode && a.text === b.text && JSON.stringify(a.parts) === JSON.stringify(b.parts) } function promptKey(binding: ReturnType[number]): PromptInfo | undefined { diff --git a/packages/opencode/src/cli/cmd/run/runtime.queue.ts b/packages/opencode/src/cli/cmd/run/runtime.queue.ts index d82b9e19e9a..79be71cadf1 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.queue.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.queue.ts @@ -102,7 +102,7 @@ export async function runPromptQueue(input: QueueInput): Promise { continue } - if (isNewCommand(prompt.text)) { + if (prompt.mode !== "shell" && isNewCommand(prompt.text)) { emit( { type: "queue", @@ -167,9 +167,11 @@ export async function runPromptQueue(input: QueueInput): Promise { break } - const commit = { kind: "user", text: prompt.text, phase: "start", source: "system" } as const - input.trace?.write("ui.commit", commit) - input.footer.append(commit) + if (prompt.mode !== "shell") { + const commit = { kind: "user", text: prompt.text, phase: "start", source: "system" } as const + input.trace?.write("ui.commit", commit) + input.footer.append(commit) + } input.onSend?.(prompt) if (state.closed) { @@ -234,7 +236,7 @@ export async function runPromptQueue(input: QueueInput): Promise { return } - if (isExitCommand(prompt.text)) { + if (prompt.mode !== "shell" && isExitCommand(prompt.text)) { input.footer.close() return } @@ -249,7 +251,7 @@ export async function runPromptQueue(input: QueueInput): Promise { queue: state.queue.length, }, ) - if (isNewCommand(prompt.text)) { + if (prompt.mode !== "shell" && isNewCommand(prompt.text)) { drain() return } diff --git a/packages/opencode/src/cli/cmd/run/runtime.ts b/packages/opencode/src/cli/cmd/run/runtime.ts index 49620e01e85..f8a29316ec3 100644 --- a/packages/opencode/src/cli/cmd/run/runtime.ts +++ b/packages/opencode/src/cli/cmd/run/runtime.ts @@ -51,6 +51,8 @@ type RunRuntimeInput = { files: RunInput["files"] initialInput?: string thinking: boolean + replay?: boolean + replayLimit?: number demo?: RunInput["demo"] } @@ -67,6 +69,8 @@ type RunLocalInput = { files: RunInput["files"] initialInput?: string thinking: boolean + replay?: boolean + replayLimit?: number demo?: RunInput["demo"] } @@ -490,6 +494,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise { directory: ctx.directory, sessionID: state.sessionID, thinking: input.thinking, + replay: input.replay, + replayLimit: input.replayLimit, limits: () => state.limits, footer, trace: log, @@ -722,6 +728,8 @@ export async function runInteractiveLocalMode(input: RunLocalInput): Promise { if (session) { @@ -774,6 +782,8 @@ export async function runInteractiveMode(input: RunInput & { createSession?: Cre files: input.files, initialInput: input.initialInput, thinking: input.thinking, + replay: input.replay, + replayLimit: input.replayLimit, demo: input.demo, boot: async () => ({ sdk: input.sdk, diff --git a/packages/opencode/src/cli/cmd/run/session-data.ts b/packages/opencode/src/cli/cmd/run/session-data.ts index 058bf236edb..a92c8c7cb61 100644 --- a/packages/opencode/src/cli/cmd/run/session-data.ts +++ b/packages/opencode/src/cli/cmd/run/session-data.ts @@ -62,13 +62,20 @@ type SessionCommit = StreamCommit // - text: part ID → full accumulated text so far // - sent: part ID → byte offset of last flushed text (for incremental output) // - end: part IDs whose time.end has arrived (part is finished) +// - shell: shell call ID → chosen transcript source for direct shell calls // - echo: message ID → bash outputs to strip from the next assistant chunk +type ShellCall = { + source: "shell" | "tool" + command?: string +} + export type SessionData = { includeUserText: boolean announced: boolean ids: Set tools: Set call: Map + shell: Map permissions: PermissionRequest[] questions: QuestionRequest[] role: Map @@ -105,6 +112,7 @@ export function createSessionData( ids: new Set(), tools: new Set(), call: new Map(), + shell: new Map(), permissions: [], questions: [], role: new Map(), @@ -622,6 +630,87 @@ function toolCommit( } } +function shellPartID(callID: string): string { + return `shell:${callID}` +} + +function claimShell(data: SessionData, callID: string, source: ShellCall["source"], command?: string): ShellCall { + const current = data.shell.get(callID) + if (current) { + if (command && !current.command) { + current.command = command + } + + return current + } + + const next = { + source, + ...(command ? { command } : {}), + } satisfies ShellCall + data.shell.set(callID, next) + return next +} + +function bashCommand(part: ToolPart): string | undefined { + if (part.tool !== "bash") { + return undefined + } + + const input = part.state.input + if (!input || typeof input !== "object" || Array.isArray(input)) { + return undefined + } + + const command = Reflect.get(input, "command") + return typeof command === "string" ? command : undefined +} + +function shellCommit( + input: { + callID: string + command: string + }, + next: Pick, +): SessionCommit { + return { + kind: "tool", + source: "tool", + partID: shellPartID(input.callID), + tool: "bash", + shell: input, + ...next, + } +} + +function startShell(callID: string, command: string): SessionCommit { + return shellCommit( + { + callID, + command, + }, + { + text: "running shell", + phase: "start", + toolState: "running", + }, + ) +} + +function doneShell(callID: string, command: string, output: string): SessionCommit { + return shellCommit( + { + callID, + command, + }, + { + text: output, + phase: "progress", + toolState: "completed", + }, + ) +} + function startTool(part: ToolPart): SessionCommit { return toolCommit(part, { text: toolStatus(part), @@ -682,6 +771,53 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput { const data = input.data const event = input.event + if (event.type === "session.next.shell.started") { + if (event.properties.sessionID !== input.sessionID) { + return out(data, commits) + } + + const shell = claimShell(data, event.properties.callID, "shell", event.properties.command) + if (shell.source !== "shell") { + return out(data, commits) + } + + const partID = shellPartID(event.properties.callID) + if (data.ids.has(partID) || data.tools.has(partID)) { + return out(data, commits, patch({ status: "running shell" })) + } + + data.tools.add(partID) + commits.push(startShell(event.properties.callID, shell.command ?? event.properties.command)) + return out(data, commits, patch({ status: "running shell" })) + } + + if (event.type === "session.next.shell.ended") { + if (event.properties.sessionID !== input.sessionID) { + return out(data, commits) + } + + const shell = claimShell(data, event.properties.callID, "shell") + if (shell.source !== "shell") { + return out(data, commits) + } + + const partID = shellPartID(event.properties.callID) + const seen = data.tools.has(partID) + const command = shell.command ?? "" + data.tools.delete(partID) + if (data.ids.has(partID)) { + return out(data, commits) + } + + if (!seen && command) { + commits.push(startShell(event.properties.callID, command)) + } + + data.ids.add(partID) + commits.push(doneShell(event.properties.callID, command, event.properties.output)) + return out(data, commits) + } + if (event.type === "message.updated") { if (event.properties.sessionID !== input.sessionID) { return out(data, commits) @@ -783,6 +919,11 @@ export function reduceSessionData(input: SessionDataInput): SessionDataOutput { if (part.type === "tool") { const view = syncPermission(data, part) ?? syncQuestion(data, part) + if (part.tool === "bash" && part.callID) { + if (claimShell(data, part.callID, "tool", bashCommand(part)).source === "shell") { + return out(data, commits, view) + } + } if (part.state.status === "running") { if (data.ids.has(part.id)) { diff --git a/packages/opencode/src/cli/cmd/run/session-replay.ts b/packages/opencode/src/cli/cmd/run/session-replay.ts new file mode 100644 index 00000000000..3fafa366cd5 --- /dev/null +++ b/packages/opencode/src/cli/cmd/run/session-replay.ts @@ -0,0 +1,188 @@ +import type { Event, PermissionRequest, QuestionRequest } from "@kilocode/sdk/v2" +import { bootstrapSessionData, createSessionData, reduceSessionData, type SessionData } from "./session-data" +import { messagePrompt, type SessionMessages } from "./session.shared" +import type { FooterPatch, StreamCommit } from "./types" + +type ReplayInput = { + messages: SessionMessages + permissions: PermissionRequest[] + questions: QuestionRequest[] + thinking: boolean + limits: Record +} + +export type SessionReplay = { + data: SessionData + commits: StreamCommit[] + patch?: FooterPatch +} + +type ReplayMessage = { + commits: StreamCommit[] + patch?: FooterPatch +} + +function apply(data: SessionData, event: Event, sessionID: string, thinking: boolean, limits: Record) { + return reduceSessionData({ + data, + event, + sessionID, + thinking, + limits, + }) +} + +function mergePatch(left: FooterPatch | undefined, right: FooterPatch | undefined) { + if (!left) { + return right + } + + if (!right) { + return left + } + + return { + ...left, + ...right, + } +} + +function active(data: SessionData) { + return data.part.size > 0 || data.tools.size > 0 +} + +function replayPatch(data: SessionData, patch: FooterPatch | undefined) { + if (active(data)) { + if (!patch) { + return { + phase: "running", + } satisfies FooterPatch + } + + return { + ...patch, + phase: "running", + } satisfies FooterPatch + } + + if (data.permissions.length > 0 || data.questions.length > 0) { + if (!patch) { + return { + phase: "idle", + } satisfies FooterPatch + } + + return { + ...patch, + phase: "idle", + } satisfies FooterPatch + } + + if (!patch) { + return undefined + } + + return { + ...patch, + phase: "idle", + status: "", + } satisfies FooterPatch +} + +function replayMessage( + data: SessionData, + message: SessionMessages[number], + thinking: boolean, + limits: Record, +): ReplayMessage { + if (message.info.role === "user") { + const prompt = messagePrompt(message) + if (!prompt.text.trim()) { + return { + commits: [], + } + } + + return { + commits: [ + { + kind: "user", + text: prompt.text, + phase: "start", + source: "system", + messageID: message.info.id, + }, + ], + } + } + + const commits: StreamCommit[] = [] + let patch: FooterPatch | undefined + + const info = apply( + data, + { + id: `bootstrap:message:${message.info.id}`, + type: "message.updated", + properties: { + sessionID: message.info.sessionID, + info: message.info, + }, + }, + message.info.sessionID, + thinking, + limits, + ) + commits.push(...info.commits) + patch = mergePatch(patch, info.footer?.patch) + + for (const part of message.parts) { + const next = apply( + data, + { + id: `bootstrap:part:${part.id}`, + type: "message.part.updated", + properties: { + sessionID: part.sessionID, + part, + time: 0, + }, + }, + message.info.sessionID, + thinking, + limits, + ) + patch = mergePatch(patch, next.footer?.patch) + commits.push(...next.commits) + } + + return { + commits, + patch, + } +} + +export function replaySession(input: ReplayInput): SessionReplay { + const data = createSessionData() + const commits: StreamCommit[] = [] + let patch: FooterPatch | undefined + + bootstrapSessionData({ + data, + messages: input.messages, + permissions: input.permissions, + questions: input.questions, + }) + + for (const message of input.messages) { + const next = replayMessage(data, message, input.thinking, input.limits) + commits.push(...next.commits) + patch = mergePatch(patch, next.patch) + } + + return { + data, + commits, + patch: replayPatch(data, patch), + } +} diff --git a/packages/opencode/src/cli/cmd/run/session.shared.ts b/packages/opencode/src/cli/cmd/run/session.shared.ts index 14e69da2ccf..7dbce26efd5 100644 --- a/packages/opencode/src/cli/cmd/run/session.shared.ts +++ b/packages/opencode/src/cli/cmd/run/session.shared.ts @@ -60,7 +60,7 @@ function fileSource( } } -function prompt(msg: SessionMessages[number]): RunPrompt { +export function messagePrompt(msg: SessionMessages[number]): RunPrompt { const parts: RunPrompt["parts"] = [] let text = msg.parts .filter((part): part is Extract => { @@ -135,7 +135,7 @@ function turn(msg: SessionMessages[number]): Turn | undefined { } return { - prompt: prompt(msg), + prompt: messagePrompt(msg), provider: msg.info.model.providerID, model: msg.info.model.modelID, variant: msg.info.model.variant, diff --git a/packages/opencode/src/cli/cmd/run/stream.transport.ts b/packages/opencode/src/cli/cmd/run/stream.transport.ts index e9e4c46e8a2..f680301e9a0 100644 --- a/packages/opencode/src/cli/cmd/run/stream.transport.ts +++ b/packages/opencode/src/cli/cmd/run/stream.transport.ts @@ -28,6 +28,7 @@ import { reduceSessionData, type SessionData, } from "./session-data" +import { replaySession } from "./session-replay" import { bootstrapSubagentCalls, bootstrapSubagentData, @@ -62,11 +63,15 @@ type Trace = { write(type: string, data?: unknown): void } +const StreamClosed = undefined as never + type StreamInput = { sdk: KiloClient directory?: string sessionID: string thinking: boolean + replay?: boolean + replayLimit?: number limits: () => Record footer: FooterApi trace?: Trace @@ -130,6 +135,8 @@ function sid(event: Event): string | undefined { } if ( + event.type === "session.next.shell.started" || + event.type === "session.next.shell.ended" || event.type === "permission.asked" || event.type === "permission.replied" || event.type === "question.asked" || @@ -410,12 +417,12 @@ function createLayer(input: StreamInput) { ), (events) => Effect.sync(() => { - void events.stream.return(undefined).catch(() => {}) + void events.stream.return(StreamClosed).catch(() => {}) }), ), ) closeStream = () => { - void events.stream.return(undefined).catch(() => {}) + void events.stream.return(StreamClosed).catch(() => {}) } input.trace?.write("recv.subscribe", { sessionID: input.sessionID, @@ -429,7 +436,12 @@ function createLayer(input: StreamInput) { blockerTick: 0, blockers: new Map(), } + let booting = true + const buffered: Event[] = [] + const replayedParts = new Set() const recovering = new Set() + const tracked = (sessionID: string | undefined) => + sessionID === input.sessionID || (!!sessionID && state.subagent.tabs.has(sessionID)) const currentSubagentState = () => { if (state.selectedSubagent && !state.subagent.tabs.has(state.selectedSubagent)) { state.selectedSubagent = undefined @@ -502,6 +514,27 @@ function createLayer(input: StreamInput) { state.footerView = current } + const resolveShellAgent = Effect.fn("RunStreamTransport.resolveShellAgent")(function* ( + agent: string | undefined, + ) { + if (agent) { + return agent + } + + const list = yield* Effect.promise(() => + input.sdk.app.agents(input.directory ? { directory: input.directory } : undefined, { throwOnError: true }), + ).pipe( + Effect.map((item) => item.data ?? []), + Effect.orElseSucceed(() => []), + ) + const next = list.find((item) => item.mode !== "subagent" && item.hidden !== true)?.name + if (next) { + return next + } + + return yield* Effect.fail(new Error("no primary agent available for shell mode")) + }) + const recoverQuestion = Effect.fn("RunStreamTransport.recoverQuestion")(function* (partID: string) { if (recovering.has(partID)) { return @@ -547,11 +580,11 @@ function createLayer(input: StreamInput) { } }) - const messages = (sessionID: string, limit: number) => + const messages = (sessionID: string, limit?: number) => Effect.promise(() => input.sdk.session.messages({ sessionID, - limit, + ...(typeof limit === "number" ? { limit } : {}), }), ).pipe( Effect.map((item) => item.data ?? []), @@ -593,7 +626,14 @@ function createLayer(input: StreamInput) { const bootstrap = Effect.fn("RunStreamTransport.bootstrap")(function* () { const [messagesList, children, permissions, questions] = yield* Effect.all( [ - messages(input.sessionID, SUBAGENT_BOOTSTRAP_LIMIT), + messages( + input.sessionID, + input.replay + ? input.replayLimit === undefined + ? undefined + : Math.max(input.replayLimit, SUBAGENT_BOOTSTRAP_LIMIT) + : SUBAGENT_BOOTSTRAP_LIMIT, + ), Effect.promise(() => input.sdk.session.children({ sessionID: input.sessionID, @@ -616,12 +656,52 @@ function createLayer(input: StreamInput) { }, ) - bootstrapSessionData({ - data: state.data, - messages: messagesList, - permissions: permissions.filter((item) => item.sessionID === input.sessionID), - questions: questions.filter((item) => item.sessionID === input.sessionID), - }) + const sessionPermissions = permissions.filter((item) => item.sessionID === input.sessionID) + const sessionQuestions = questions.filter((item) => item.sessionID === input.sessionID) + const history = input.replay + ? replaySession({ + messages: messagesList, + permissions: sessionPermissions, + questions: sessionQuestions, + thinking: input.thinking, + limits: input.limits(), + }) + : undefined + const replay = + history && input.replayLimit !== undefined && messagesList.length > input.replayLimit + ? replaySession({ + messages: messagesList.slice(-input.replayLimit), + permissions: sessionPermissions, + questions: sessionQuestions, + thinking: input.thinking, + limits: input.limits(), + }) + : history + + replayedParts.clear() + if (history) { + state.data = history.data + } + + if (!history) { + bootstrapSessionData({ + data: state.data, + messages: messagesList, + permissions: sessionPermissions, + questions: sessionQuestions, + }) + } + + if (replay) { + for (const [partID] of replay.data.text) { + if (!replay.data.part.has(partID)) { + continue + } + + replayedParts.add(partID) + } + } + bootstrapSubagentData({ data: state.subagent, messages: messagesList, @@ -629,6 +709,7 @@ function createLayer(input: StreamInput) { permissions, questions, }) + clearFinishedSubagents(state.subagent) for (const request of [ ...state.data.permissions, @@ -639,9 +720,29 @@ function createLayer(input: StreamInput) { seedBlocker(request.id) } + if (replay) { + const activeCommitIDs = new Set([...state.data.part.keys(), ...state.data.tools]) + for (const commit of replay.commits) { + input.trace?.write("ui.commit", commit) + input.footer.append(commit) + + if (commit.partID && activeCommitIDs.has(commit.partID)) { + continue + } + + yield* Effect.promise(() => input.footer.idle()).pipe(Effect.orElseSucceed(() => undefined)) + } + } + const snapshot = currentSubagentState() traceTabs(input.trace, [], snapshot.tabs) - syncFooter([], undefined, snapshot) + syncFooter([], replay?.patch, snapshot) + if (replay) { + yield* Effect.promise(() => input.footer.idle()).pipe(Effect.orElseSucceed(() => undefined)) + } + + booting = false + yield* drainBuffered() const sessions = [...state.subagent.tabs.keys()] if (sessions.length === 0) { @@ -735,6 +836,86 @@ function createLayer(input: StreamInput) { }) } + const applyEvent = Effect.fn("RunStreamTransport.applyEvent")(function* (event: Event) { + if (event.type === "message.part.delta" && event.properties.sessionID === input.sessionID) { + if (replayedParts.has(event.properties.partID)) { + const seen = state.data.text.get(event.properties.partID) ?? "" + if (seen.endsWith(event.properties.delta)) { + return + } + + replayedParts.delete(event.properties.partID) + } + } + + trackBlocker(event) + + const prev = event.type === "message.part.updated" ? listSubagentTabs(state.subagent) : undefined + const next = reduceSessionData({ + data: state.data, + event, + sessionID: input.sessionID, + thinking: input.thinking, + limits: input.limits(), + }) + state.data = next.data + + if ( + event.type === "message.part.updated" && + event.properties.part.sessionID === input.sessionID && + event.properties.part.type === "tool" && + event.properties.part.tool === "question" && + event.properties.part.state.status === "running" && + state.data.questions.length === 0 + ) { + yield* recoverQuestion(event.properties.part.id).pipe( + Effect.forkIn(scope, { startImmediately: true }), + Effect.asVoid, + ) + } + + const changed = reduceSubagentData({ + data: state.subagent, + event, + sessionID: input.sessionID, + thinking: input.thinking, + limits: input.limits(), + }) + if (changed && prev) { + traceTabs(input.trace, prev, listSubagentTabs(state.subagent)) + } + releaseBlocker(event) + + syncFooter(next.commits, next.footer?.patch, changed ? currentSubagentState() : undefined) + + touch(event) + yield* mark(event) + }) + + const drainBuffered = Effect.fn("RunStreamTransport.drainBuffered")(function* () { + let pending = buffered.splice(0) + while (pending.length > 0) { + const next: Event[] = [] + let changed = false + for (const event of pending) { + if (!tracked(sid(event))) { + next.push(event) + continue + } + + changed = true + yield* applyEvent(event) + } + + if (!changed) { + buffered.push(...next) + return + } + + pending = next + } + }) + const watch = Effect.fn("RunStreamTransport.watch")(() => Stream.fromAsyncIterable(events.stream, (error) => error instanceof Error ? error : new Error(String(error)), @@ -759,53 +940,25 @@ function createLayer(input: StreamInput) { } const sessionID = sid(event) - if (sessionID !== input.sessionID && (!sessionID || !state.subagent.tabs.has(sessionID))) { + if (booting) { + if (sessionID) { + input.trace?.write("recv.event", event) + buffered.push(event) + } + return + } + + if (!tracked(sessionID)) { + if (sessionID) { + input.trace?.write("recv.event", event) + buffered.push(event) + } return } input.trace?.write("recv.event", event) - trackBlocker(event) - - const prev = event.type === "message.part.updated" ? listSubagentTabs(state.subagent) : undefined - const next = reduceSessionData({ - data: state.data, - event, - sessionID: input.sessionID, - thinking: input.thinking, - limits: input.limits(), - }) - state.data = next.data - - if ( - event.type === "message.part.updated" && - event.properties.part.sessionID === input.sessionID && - event.properties.part.type === "tool" && - event.properties.part.tool === "question" && - event.properties.part.state.status === "running" && - state.data.questions.length === 0 - ) { - yield* recoverQuestion(event.properties.part.id).pipe( - Effect.forkIn(scope, { startImmediately: true }), - Effect.asVoid, - ) - } - - const changed = reduceSubagentData({ - data: state.subagent, - event, - sessionID: input.sessionID, - thinking: input.thinking, - limits: input.limits(), - }) - if (changed && prev) { - traceTabs(input.trace, prev, listSubagentTabs(state.subagent)) - } - releaseBlocker(event) - - syncFooter(next.commits, next.footer?.patch, changed ? currentSubagentState() : undefined) - - touch(event) - yield* mark(event) + yield* applyEvent(event) + yield* drainBuffered() }), ), Effect.catch((error) => (abort.signal.aborted ? Effect.void : fail(error))), @@ -820,8 +973,8 @@ function createLayer(input: StreamInput) { ), ) - yield* bootstrap() yield* Scope.provide(scope)(watch().pipe(Effect.forkScoped)) + yield* bootstrap() const runPromptTurn = Effect.fn("RunStreamTransport.runPromptTurn")(function* (next: SessionTurnInput) { if (closed || next.signal?.aborted || input.footer.isClosed) { @@ -874,66 +1027,108 @@ function createLayer(input: StreamInput) { ], } const command = next.prompt.command - const send = command - ? Effect.sync(() => { - input.trace?.write("send.command", { sessionID: input.sessionID, command: command.name }) - }).pipe( - Effect.andThen( - Effect.promise(() => - input.sdk.session.command( - { - sessionID: input.sessionID, - agent: next.agent, - model: next.model ? `${next.model.providerID}/${next.model.modelID}` : undefined, - variant: next.variant, - command: command.name, - arguments: command.arguments, - parts: [ - ...(next.includeFiles ? next.files : []), - ...next.prompt.parts.filter( - (item): item is Extract => item.type === "file", + const send = + next.prompt.mode === "shell" + ? Effect.sync(() => { + input.trace?.write("send.shell", { + sessionID: input.sessionID, + command: next.prompt.text, + }) + }).pipe( + Effect.andThen( + resolveShellAgent(next.agent) + .pipe( + Effect.flatMap((agent) => + Effect.promise(() => + input.sdk.session.shell( + { + sessionID: input.sessionID, + agent, + model: next.model, + command: next.prompt.text, + }, + { signal: turn.signal, throwOnError: true }, + ), ), - ], - }, - { signal: turn.signal }, + ), + ) + .pipe( + Effect.tap(() => + Effect.sync(() => { + input.trace?.write("send.shell.ok", { + sessionID: input.sessionID, + }) + item.armed = true + item.live = true + }), + ), + Effect.flatMap(() => Deferred.succeed(item.done, undefined).pipe(Effect.ignore)), + Effect.catch((error) => Deferred.fail(item.done, error).pipe(Effect.ignore)), + Effect.forkIn(scope, { startImmediately: true }), + Effect.asVoid, + ), + ), + ) + : command + ? Effect.sync(() => { + input.trace?.write("send.command", { sessionID: input.sessionID, command: command.name }) + }).pipe( + Effect.andThen( + Effect.promise(() => + input.sdk.session.command( + { + sessionID: input.sessionID, + agent: next.agent, + model: next.model ? `${next.model.providerID}/${next.model.modelID}` : undefined, + variant: next.variant, + command: command.name, + arguments: command.arguments, + parts: [ + ...(next.includeFiles ? next.files : []), + ...next.prompt.parts.filter( + (item): item is Extract => item.type === "file", + ), + ], + }, + { signal: turn.signal }, + ), + ).pipe( + Effect.tap(() => + Effect.sync(() => { + input.trace?.write("send.command.ok", { + sessionID: input.sessionID, + command: command.name, + }) + item.armed = true + item.live = true + }), + ), + Effect.flatMap(() => Deferred.succeed(item.done, undefined).pipe(Effect.ignore)), + Effect.catch((error) => Deferred.fail(item.done, error).pipe(Effect.ignore)), + Effect.forkIn(scope, { startImmediately: true }), + Effect.asVoid, + ), + ), + ) + : Effect.sync(() => { + input.trace?.write("send.prompt", req) + }).pipe( + Effect.andThen( + Effect.promise(() => + input.sdk.session.promptAsync(req, { + signal: turn.signal, + }), + ), ), - ).pipe( Effect.tap(() => Effect.sync(() => { - input.trace?.write("send.command.ok", { + input.trace?.write("send.prompt.ok", { sessionID: input.sessionID, - command: command.name, }) item.armed = true - item.live = true }), ), - Effect.flatMap(() => Deferred.succeed(item.done, undefined).pipe(Effect.ignore)), - Effect.catch((error) => Deferred.fail(item.done, error).pipe(Effect.ignore)), - Effect.forkIn(scope, { startImmediately: true }), - Effect.asVoid, - ), - ), - ) - : Effect.sync(() => { - input.trace?.write("send.prompt", req) - }).pipe( - Effect.andThen( - Effect.promise(() => - input.sdk.session.promptAsync(req, { - signal: turn.signal, - }), - ), - ), - Effect.tap(() => - Effect.sync(() => { - input.trace?.write("send.prompt.ok", { - sessionID: input.sessionID, - }) - item.armed = true - }), - ), - ) + ) yield* send.pipe( Effect.flatMap(() => { diff --git a/packages/opencode/src/cli/cmd/run/subagent-data.ts b/packages/opencode/src/cli/cmd/run/subagent-data.ts index 7fef1d9e975..759a85ff245 100644 --- a/packages/opencode/src/cli/cmd/run/subagent-data.ts +++ b/packages/opencode/src/cli/cmd/run/subagent-data.ts @@ -421,9 +421,26 @@ function ensureBlockerTab( title: string | undefined, kind: "permission" | "question", ) { - if (data.tabs.has(sessionID)) { + const current = data.tabs.get(sessionID) + if (current) { ensureDetail(data, sessionID) - return false + if (current.status !== "running") { + return false + } + + const next = { + ...current, + description: kind === "permission" ? "Pending permission" : "Pending question", + status: "running" as const, + title: current.title ?? title, + lastUpdatedAt: Date.now(), + } + if (sameSubagentTab(current, next)) { + return false + } + + data.tabs.set(sessionID, next) + return true } data.tabs.set(sessionID, { diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index 0afd841f7d3..0c6775ebbfd 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -35,7 +35,7 @@ import { webSearchProviderLabel, type WebSearchTool } from "@/tool/websearch" import type { WriteTool } from "@/tool/write" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import * as Locale from "@/util/locale" -import type { RunDiffStyle, RunEntryBody, StreamCommit, ToolSnapshot } from "./types" +import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types" export type ToolView = { output: boolean @@ -626,6 +626,10 @@ function scrollBashStart(p: ToolProps): string { const desc = p.input.description || "Shell" const wd = p.input.workdir ?? "" const dir = wd && wd !== "." ? toolPath(wd) : "" + if (cmd && desc === "Shell" && !dir) { + return `$ ${cmd}` + } + const title = dir && !desc.includes(dir) ? `${desc} in ${dir}` : desc if (!cmd) { @@ -1248,7 +1252,7 @@ function frame(part: ToolPart): ToolFrame { raw: "", name: part.tool, input: dict(state.input), - meta: dict(state.metadata), + meta: "metadata" in part.state ? dict(part.state.metadata) : {}, state, status: text(state.status), error: text(state.error), @@ -1261,7 +1265,7 @@ export function toolFrame(commit: StreamCommit, raw: string): ToolFrame { raw, name: commit.tool || commit.part?.tool || "tool", input: dict(state.input), - meta: dict(state.metadata), + meta: commit.part?.state && "metadata" in commit.part.state ? dict(commit.part.state.metadata) : {}, state, status: commit.toolState ?? text(state.status), error: (commit.toolError ?? "").trim(), @@ -1403,7 +1407,32 @@ function structuredBody(commit: StreamCommit, raw: string): RunEntryBody | undef } } +function shellOutput(command: string, raw: string): string | undefined { + const body = stripAnsi(raw).replace(/^\n+/, "").replace(/\n+$/, "") + if (!body) { + return undefined + } + + if (!command) { + return body + } + + return `\n${body}` +} + export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | undefined { + if (commit.shell) { + if (commit.phase === "start") { + return textBody(`$ ${commit.shell.command}`) + } + + if (commit.phase === "progress") { + return textBody(shellOutput(commit.shell.command, raw) ?? "") + } + + return undefined + } + const ctx = toolFrame(commit, raw) const view = toolView(ctx.name) diff --git a/packages/opencode/src/cli/cmd/run/types.ts b/packages/opencode/src/cli/cmd/run/types.ts index e6709d6dcda..d1d641bfb41 100644 --- a/packages/opencode/src/cli/cmd/run/types.ts +++ b/packages/opencode/src/cli/cmd/run/types.ts @@ -34,6 +34,7 @@ export type RunProvider = NonNullable Server.listen(opts)) - console.log(`kilo server listening on http://${server.hostname}:${server.port}`) // kilocode_change + + // kilocode_change start + const urls = server.urls + + console.log(`kilo server listening on ${urls.bind}`) + if (urls.network) { + console.log(` Local: ${urls.local}`) + console.log(` Network: ${urls.network}`) + } + // kilocode_change end // kilocode_change start - graceful signal shutdown // yield* Effect.never diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 98c8f1f0cde..d557217d462 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,9 +1,9 @@ -import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" // kilocode_change +import { render, TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid" import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import * as Clipboard from "@tui/util/clipboard" import * as Selection from "@tui/util/selection" import * as TuiAudio from "@tui/util/audio" -import { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } from "@opentui/core" // kilocode_change +import { createCliRenderer, MouseButton, type CliRenderer, type CliRendererConfig } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, @@ -19,14 +19,14 @@ import { on, untrack, // kilocode_change } from "solid-js" -import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" // kilocode_change +import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" import { Flag } from "@opencode-ai/core/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" -import { InstallationVersion } from "@opencode-ai/core/installation/version" // kilocode_change +import { ErrorComponent } from "@tui/component/error-component" import { PluginRouteMissing } from "@tui/component/plugin-route-missing" -import { ProjectProvider } from "@tui/context/project" +import { ProjectProvider, useProject } from "@tui/context/project" import { EditorContextProvider } from "@tui/context/editor" import { useEvent } from "@tui/context/event" import { SDKProvider, useSDK } from "@tui/context/sdk" @@ -40,8 +40,10 @@ import { DialogMcp } from "@tui/component/dialog-mcp" import { DialogStatus } from "@tui/component/dialog-status" import { DialogThemeList } from "@tui/component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" +import { DialogHeadlessLink } from "@/kilocode/cli/cmd/tui/component/dialog-headless-link" // kilocode_change import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" +import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list" import { DialogConsoleOrg } from "@tui/component/dialog-console-org" import { ThemeProvider, useTheme } from "@tui/context/theme" import { Home } from "@tui/routes/home" @@ -52,11 +54,7 @@ import { PromptStashProvider } from "./component/prompt/stash" import { DialogAlert } from "./ui/dialog-alert" import { DialogConfirm } from "./ui/dialog-confirm" import { ToastProvider, useToast } from "./ui/toast" -import { ExitProvider, useExit } from "./context/exit" -// kilocode_change start -import { DialogSelect } from "./ui/dialog-select" -import { Link } from "./ui/link" -// kilocode_change end +import { createExit, ExitProvider, useExit, type Exit } from "./context/exit" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" import { Provider } from "@/provider/provider" @@ -72,15 +70,21 @@ import type { RouteMap } from "@/cli/cmd/tui/plugin/api" import { createTuiAttention } from "@/cli/cmd/tui/attention" import { FormatError, FormatUnknownError } from "@/cli/error" import { kitty, resetTerminalState } from "@/kilocode/cli/cmd/tui/util/terminal" // kilocode_change -import * as AppExit from "@/kilocode/tui/app-exit" // kilocode_change -import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette" -import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap" +import { hasDisplay } from "@/kilocode/cli/cmd/tui/util/display" // kilocode_change +import { CommandPaletteDialog } from "./component/command-palette" +import { + COMMAND_PALETTE_COMMAND, + KILO_BASE_MODE, + OpencodeKeymapProvider, + registerOpencodeKeymap, + useBindings, + useOpencodeKeymap, +} from "./keymap" import type { EventSource } from "./context/sdk" import { DialogVariant } from "./component/dialog-variant" -const appBindingCommands = [ - "command.palette.show", +const appGlobalBindingCommands = [ "session.list", "session.new", "session.quick_switch.1", @@ -92,6 +96,10 @@ const appBindingCommands = [ "session.quick_switch.7", "session.quick_switch.8", "session.quick_switch.9", +] as const + +const appBindingCommands = [ + "command.palette.show", "model.list", "model.cycle_recent", "model.cycle_recent_reverse", @@ -111,6 +119,7 @@ const appBindingCommands = [ "theme.mode.lock", "help.show", "docs.open", + "workspace.list", "app.debug", "app.console", "app.heap_snapshot", @@ -123,7 +132,7 @@ const appBindingCommands = [ "app.toggle.session_directory_filter", ] as const -function rendererConfig(_config: TuiConfig.Resolved): CliRendererConfig { +export function tuiRendererConfig(_config: TuiConfig.Resolved): CliRendererConfig { const mouseEnabled = !Flag.KILO_DISABLE_MOUSE && (_config.mouse ?? true) const keyboard = kitty() // kilocode_change @@ -147,6 +156,34 @@ function rendererConfig(_config: TuiConfig.Resolved): CliRendererConfig { } } +export function createTuiRenderer(config: TuiConfig.Resolved) { + return createCliRenderer(tuiRendererConfig(config)) +} + +export type TuiHandle = { + ready: Promise + done: Promise + exit: Exit +} + +type TuiInput = { + url: string + args: Args + config: TuiConfig.Resolved + renderer: CliRenderer + onSnapshot?: () => Promise + directory?: string + fetch?: typeof fetch + headers?: RequestInit["headers"] + events?: EventSource +} + +type TuiLifecycle = { + exit: Exit + exited: Promise + fail(error: unknown): Promise +} + function errorMessage(error: unknown) { const formatted = FormatError(error) if (formatted !== undefined) return formatted @@ -164,109 +201,181 @@ function errorMessage(error: unknown) { return FormatUnknownError(error) } -export function tui(input: { - url: string - args: Args - config: TuiConfig.Resolved - onSnapshot?: () => Promise - directory?: string - fetch?: typeof fetch - headers?: RequestInit["headers"] - events?: EventSource -}) { - // promise to prevent immediate exit - // oxlint-disable-next-line no-async-promise-executor -- intentional: async executor used for sequential setup before resolve - return new Promise(async (resolve) => { - const unguard = win32InstallCtrlCGuard() - win32DisableProcessedInput() +export function tui(input: TuiInput): TuiHandle { + const unguard = win32InstallCtrlCGuard() + win32DisableProcessedInput() - const onExit = async () => { - unguard?.() - resolve() - } - const onBeforeExit = async () => { - offKeymap() + process.on("exit", resetTerminalState) // kilocode_change + + const renderer = input.renderer + const keymap = createDefaultOpenTuiKeymap(renderer) + const unregisterKeymap = registerOpencodeKeymap(keymap, renderer, input.config) + const lifecycle = createTuiLifecycle({ + renderer, + unguard, + cleanup: async () => { + unregisterKeymap() await TuiPluginRuntime.dispose() TuiAudio.dispose() - } - - process.on("exit", resetTerminalState) // kilocode_change - - const renderer = await createCliRenderer(rendererConfig(input.config)) - // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash. - void renderer.getPalette({ size: 16 }).catch(() => undefined) - const mode = (await renderer.waitForThemeMode(1000)) ?? "dark" - - const keymap = createDefaultOpenTuiKeymap(renderer) - const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config) - - await render(() => { - return ( - ( - - )} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) - }, renderer) + }, }) + const ready = mountTui({ ...input, keymap, exit: lifecycle.exit }).catch((error) => lifecycle.fail(error)) + const done = waitUntilDone(ready, lifecycle.exited) + + return { ready, done, exit: lifecycle.exit } +} + +async function mountTui(input: TuiInput & { keymap: ReturnType; exit: Exit }) { + const renderer = input.renderer + // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash. + void renderer.getPalette({ size: 16 }).catch(() => undefined) + const mode = (await renderer.waitForThemeMode(1000)) ?? "dark" + if (renderer.isDestroyed) return + + await render(() => { + return ( + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + }, renderer) +} + +function createTuiLifecycle(input: { + renderer: CliRenderer + unguard?: () => void + cleanup: () => Promise +}): TuiLifecycle { + let resolveExited!: () => void + const exited = new Promise((resolve) => { + resolveExited = resolve + }) + let exitCompleted = false + let exiting = false + let cleanupTask: Promise | undefined + + const completeExit = () => { + if (exitCompleted) return + exitCompleted = true + resolveExited() + } + + const cleanup = () => { + cleanupTask ??= (async () => { + process.off("SIGHUP", onSighup) + try { + await input.cleanup() + } finally { + input.unguard?.() + } + })() + return cleanupTask + } + + const exit = createExit(async (reason, message) => { + exiting = true + await cleanup() + if (!input.renderer.isDestroyed) { + input.renderer.setTerminalTitle("") + input.renderer.destroy() + } + win32FlushInputBuffer() + resetTerminalState() // kilocode_change + if (reason) { + const formatted = FormatError(reason) ?? FormatUnknownError(reason) + if (formatted) process.stderr.write(formatted + "\n") + } + const text = message() + if (text) process.stdout.write(text + "\n") + completeExit() + }) + const onSighup = () => { + void exit() + } + + input.renderer.once("destroy", () => { + if (exiting) return + void cleanup().finally(() => { + win32FlushInputBuffer() + resetTerminalState() // kilocode_change + completeExit() + }) + }) + process.on("SIGHUP", onSighup) + + return { + exit, + exited, + async fail(error) { + exiting = true + await cleanup().catch(() => {}) + if (!input.renderer.isDestroyed) input.renderer.destroy() + win32FlushInputBuffer() // kilocode_change + resetTerminalState() // kilocode_change + completeExit() + throw error + }, + } +} + +async function waitUntilDone(ready: Promise, exited: Promise) { + await ready + await exited } function App(props: { onSnapshot?: () => Promise }) { @@ -277,7 +386,6 @@ function App(props: { onSnapshot?: () => Promise }) { const dialog = useDialog() const local = useLocal() const kv = useKV() - const command = useCommandPalette() const keymap = useOpencodeKeymap() const event = useEvent() const sdk = useSDK() @@ -285,6 +393,7 @@ function App(props: { onSnapshot?: () => Promise }) { const themeState = useTheme() const { theme, mode, setMode, locked, lock, unlock } = themeState const sync = useSync() + const project = useProject() const exit = useExit() const promptRef = usePromptRef() const routes: RouteMap = new Map() @@ -458,15 +567,22 @@ function App(props: { onSnapshot?: () => Promise }) { ) const connected = useConnected() + const currentWorktreeWorkspace = createMemo(() => { + const workspaceID = project.workspace.current() + if (!workspaceID) return + const workspace = project.workspace.get(workspaceID) + if (workspace?.type !== "worktree" || !workspace.directory) return + return workspace + }) const appCommands = createMemo(() => [ { - name: "command.palette.show", + name: COMMAND_PALETTE_COMMAND, title: "Show command palette", category: "System", hidden: true, run: () => { - command.show() + dialog.replace(() => ) }, }, { @@ -494,6 +610,30 @@ function App(props: { onSnapshot?: () => Promise }) { dialog.clear() }, }, + { + name: "workspace.copy_path", + title: "Copy worktree path", + category: "Workspace", + enabled: () => currentWorktreeWorkspace() !== undefined, + run: async () => { + const workspace = currentWorktreeWorkspace() + if (!workspace?.directory) return + await Clipboard.copy(workspace.directory) + .then(() => toast.show({ message: "Copied worktree path", variant: "info" })) + .catch(toast.error) + dialog.clear() + }, + }, + { + name: "workspace.list", + title: "Manage workspaces", + category: "Workspace", + hidden: !Flag.KILO_EXPERIMENTAL_WORKSPACES, + slashName: "workspaces", + run: () => { + dialog.replace(() => ) + }, + }, ...Array.from({ length: 9 }, (_, i) => ({ name: `session.quick_switch.${i + 1}`, title: `Switch to session in quick slot ${i + 1}`, @@ -678,12 +818,25 @@ function App(props: { onSnapshot?: () => Promise }) { name: "docs.open", title: "Open docs", run: () => { - open(KiloApp.DOCS_URL).catch(() => {}) // kilocode_change + // kilocode_change start + if (!hasDisplay()) { + DialogHeadlessLink.show(dialog, KiloApp.DOCS_URL) + return + } + open(KiloApp.DOCS_URL).catch(() => {}) dialog.clear() + // kilocode_change end }, category: "System", }, - AppExit.command(exit), // kilocode_change + { + name: "app.exit", + title: "Exit the app", + slashName: "exit", + slashAliases: ["quit", "q"], + run: () => exit(), + category: "System", + }, { name: "app.debug", title: "Toggle debug panel", @@ -809,19 +962,28 @@ function App(props: { onSnapshot?: () => Promise }) { })) useBindings(() => ({ - enabled: command.matcher, + mode: KILO_BASE_MODE, bindings: tuiConfig.keybinds.gather("app", appBindingCommands), })) useBindings(() => ({ - enabled: () => AppExit.enabled(command.matcher.get(), promptRef.current), // kilocode_change + bindings: tuiConfig.keybinds.gather("app.global", appGlobalBindingCommands), + })) + + useBindings(() => ({ + mode: KILO_BASE_MODE, + enabled: () => { + const current = promptRef.current + if (!current?.focused) return true + return current.current.input === "" + }, bindings: tuiConfig.keybinds.gather("app_exit", ["app.exit"]), })) KiloApp.init() // kilocode_change event.on(TuiEvent.CommandExecute.type, (evt) => { - command.run(evt.properties.command) + keymap.dispatchCommand(evt.properties.command) }) event.on(TuiEvent.ToastShow.type, (evt) => { @@ -964,115 +1126,3 @@ function App(props: { onSnapshot?: () => Promise }) { ) } - -// kilocode_change start — guard against missing renderer context in ErrorBoundary fallback -function tryUseRenderer() { - try { - return useRenderer() - } catch { - return undefined - } -} - -function tryUseTerminalDimensions() { - try { - return useTerminalDimensions() - } catch { - return undefined - } -} - -function ErrorComponent(props: { - error: Error - reset: () => void - onBeforeExit?: () => Promise - onExit: () => Promise - mode?: "dark" | "light" -}) { - const term = tryUseTerminalDimensions() - const renderer = tryUseRenderer() - - const height = () => term?.().height ?? process.stdout.rows ?? 24 - - const handleExit = async () => { - await props.onBeforeExit?.() - renderer?.setTerminalTitle("") - renderer?.destroy() - win32FlushInputBuffer() - resetTerminalState() - await props.onExit() - } - - try { - useKeyboard((evt) => { - if (evt.ctrl && evt.name === "c") { - handleExit() - } - }) - } catch { - // Keyboard handler unavailable — renderer context may be missing. - // Ctrl+C will still work via the default signal handler. - } - - const [copied, setCopied] = createSignal(false) - - const issueURL = new URL("https://github.com/Kilo-Org/kilocode/issues/new?template=bug-report.yml") - - // Choose safe fallback colors per mode since theme context may not be available - const isLight = props.mode === "light" - const colors = { - bg: isLight ? "#ffffff" : "#0a0a0a", - text: isLight ? "#1a1a1a" : "#eeeeee", - muted: isLight ? "#8a8a8a" : "#808080", - primary: isLight ? "#3b7dd8" : "#fab283", - } - - if (props.error.message) { - issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`) - } - - if (props.error.stack) { - issueURL.searchParams.set( - "description", - "```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```", - ) - } - - issueURL.searchParams.set("opencode-version", InstallationVersion) - - const copyIssueURL = () => { - Clipboard.copy(issueURL.toString()).then(() => { - setCopied(true) - }) - } - - return ( - - - - Please report an issue. - - - - Copy issue URL (exception info pre-filled) - - - {copied() && Successfully copied} - - - A fatal error occurred! - - Reset TUI - - - Exit - - - - {props.error.stack} - - {props.error.message} - - ) -} -// kilocode_change end diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index eee28b1de9c..65c5d3bfb9c 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -100,7 +100,6 @@ export const AttachCommand = cmd({ } // kilocode_change end const config = await TuiConfig.get() - const { tui } = await import("./app") try { await validateSession({ @@ -115,9 +114,12 @@ export const AttachCommand = cmd({ return } - await tui({ + const { createTuiRenderer, tui } = await import("./app") + const renderer = await createTuiRenderer(config) + const handle = tui({ url: args.url, config, + renderer, args: { continue: args.continue, sessionID: args.session, @@ -126,6 +128,7 @@ export const AttachCommand = cmd({ directory, headers, }) + await handle.done } finally { unguard?.() } diff --git a/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx new file mode 100644 index 00000000000..0d6deeedcde --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx @@ -0,0 +1,79 @@ +import { createMemo } from "solid-js" +import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" +import { type DialogContext } from "@tui/ui/dialog" +import { + COMMAND_PALETTE_COMMAND, + formatKeyBindings, + type OpenTuiKeymap, + useKeymapSelector, + useOpencodeKeymap, +} from "../keymap" +import { useTuiConfig } from "../context/tui-config" + +type PaletteCommandEntry = ReturnType[number] + +function isVisiblePaletteCommand(command: PaletteCommandEntry["command"]) { + return command.hidden !== true && command.name !== COMMAND_PALETTE_COMMAND +} + +function isSuggestedPaletteCommand(entry: PaletteCommandEntry) { + const suggested = entry.command.suggested + if (typeof suggested === "boolean") return suggested + if (typeof suggested === "function") return suggested() === true + return false +} + +export function CommandPaletteDialog() { + const config = useTuiConfig() + const keymap = useOpencodeKeymap() + const entries = useKeymapSelector((keymap: OpenTuiKeymap) => { + const query = { + namespace: "palette", + } + const reachable = keymap.getCommandEntries({ + ...query, + visibility: "reachable", + filter: isVisiblePaletteCommand, + }) + const registeredBindings = keymap.getCommandBindings({ + visibility: "registered", + commands: reachable.map((entry) => entry.command.name), + }) + + return reachable.map((entry) => ({ + ...entry, + bindings: registeredBindings.get(entry.command.name) ?? entry.bindings, + })) + }) + const options = createMemo(() => + entries().map((entry) => ({ + title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name, + description: typeof entry.command.desc === "string" ? entry.command.desc : undefined, + category: typeof entry.command.category === "string" ? entry.command.category : undefined, + footer: formatKeyBindings(entry.bindings, config), + value: entry.command.name, + suggested: isSuggestedPaletteCommand(entry), + onSelect: (dialog: DialogContext) => { + dialog.clear() + keymap.dispatchCommand(entry.command.name) + }, + })), + ) + + let ref: DialogSelectRef + const list = () => { + if (ref?.filter) return options() + return [ + ...options() + .filter((option) => option.suggested) + .map((option) => ({ + ...option, + value: `suggested:${option.value}`, + category: "Suggested", + })), + ...options(), + ] + } + + return (ref = value)} title="Commands" options={list()} /> +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 2a3378ed963..1e7b9ebe573 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -64,9 +64,12 @@ export function DialogModel(props: { providerID?: string }) { }) const footer = (providerID: string, model: Model) => { - if (providerID === "kilo" && FreeModelDisclosure.collectsData(model)) return FreeModelDisclosure.label - if (model.cost?.input === 0 && providerID === "opencode") return "Free" - return undefined + const labels = [ + providerID === "kilo" && FreeModelDisclosure.hasByok(model) ? FreeModelDisclosure.byok : undefined, + providerID === "kilo" && FreeModelDisclosure.collectsData(model) ? FreeModelDisclosure.label : undefined, + model.cost?.input === 0 && providerID === "opencode" ? "Free" : undefined, + ].filter((label) => label !== undefined) + return labels.length > 0 ? labels.join(" · ") : undefined } // kilocode_change end @@ -123,6 +126,7 @@ export function DialogModel(props: { providerID?: string }) { map(([model, info]) => ({ value: { providerID: provider.id, modelID: model }, title: info.name ?? model, + releaseDate: info.release_date, description: favorites.some((item) => item.providerID === provider.id && item.modelID === model) ? "(Favorite)" : undefined, @@ -150,15 +154,7 @@ export function DialogModel(props: { providerID?: string }) { // kilocode_change end return true }), - sortBy( - // kilocode_change start - Sort within Recommended / Kilo Gateway - (x) => (x.value.providerID === "kilo" ? (kiloRank().get(x.value.modelID) ?? Infinity) : 0), - // kilocode_change end - // kilocode_change start - free model footers include Kilo disclosure labels - (x) => x.footer === undefined, - // kilocode_change end - (x) => x.title, // kilocode_change - ), // kilocode_change + (options) => sortModelOptions(options, props.providerID !== undefined, kiloRank()), // kilocode_change ), ), ) @@ -263,3 +259,34 @@ export function DialogModel(props: { providerID?: string }) { ) // kilocode_change end } + +export function sortModelOptions< + T extends { + footer?: string + releaseDate: string + title: string + value?: { providerID: string; modelID: string } // kilocode_change + }, +>( + options: T[], + newestFirst: boolean, + rank: ReadonlyMap = new Map(), // kilocode_change +) { + // kilocode_change start - Sort within Recommended / Kilo Gateway + const recommended = (option: T) => + option.value?.providerID === "kilo" ? (rank.get(option.value.modelID) ?? Infinity) : 0 + // kilocode_change end + if (newestFirst) + return sortBy( + options, + recommended, // kilocode_change + [(option) => option.releaseDate, "desc"], + (option) => option.title, + ) + return sortBy( + options, + recommended, // kilocode_change + (option) => option.footer === undefined, // kilocode_change - free model footers include Kilo disclosure labels + (option) => option.title, + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 3d15fce24cb..e45775115ad 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -277,6 +277,13 @@ function AutoMethod(props: AutoMethodProps) { method: props.index, }) if (result.error) { + toast.show({ + variant: "error", + message: + "name" in result.error && result.error.name === "ProviderAuthOauthCallbackFailed" + ? "OAuth authorization failed. Try /connect again." + : JSON.stringify(result.error), + }) dialog.clear() return } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index b42966dd8ed..2fa6350ee5c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -69,13 +69,22 @@ export function DialogSessionList() { const workspaceID = await (async () => { if (selection.type === "none") return null if (selection.type === "existing") return selection.workspaceID - const result = await sdk.client.experimental.workspace - .create({ type: selection.workspaceType, branch: null }) - .catch(() => undefined) + let result + try { + result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null }) + } catch (err) { + toast.show({ + title: "Failed to create workspace", + message: errorMessage(err), + variant: "error", + }) + return + } const workspace = result?.data if (!workspace) { toast.show({ - message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`, + title: "Failed to create workspace", + message: errorMessage(result?.error ?? "no response"), variant: "error", }) return diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx index 6d6c62450ea..1d58401ebc9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-tag.tsx @@ -1,12 +1,14 @@ import { createMemo, createResource } from "solid-js" import { DialogSelect } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" +import { useProject } from "@tui/context/project" import { useSDK } from "@tui/context/sdk" import { createStore } from "solid-js/store" export function DialogTag(props: { onSelect?: (value: string) => void }) { const sdk = useSDK() const dialog = useDialog() + const project = useProject() const [store] = createStore({ filter: "", @@ -17,6 +19,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) { async () => { const result = await sdk.client.find.files({ query: store.filter, + workspace: project.workspace.current(), }) if (result.error) return [] const sliced = (result.data ?? []).slice(0, 5) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index a3c38e91098..a5ac6c0de38 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -61,15 +61,17 @@ async function loadWorkspaceAdapters(input: { const dir = input.sync.path.directory || input.sdk.directory const url = new URL("/experimental/workspace/adapter", input.sdk.url) if (dir) url.searchParams.set("directory", dir) - const res = await input.sdk - .fetch(url) - .then((x) => x.json() as Promise) - .catch(() => undefined) - if (res) return res - input.toast.show({ - message: "Failed to load workspace adapters", - variant: "error", - }) + try { + const response = await input.sdk.fetch(url) + return (await response.json()) as Adapter[] + } catch (err) { + input.toast.show({ + title: "Failed to load workspace adapters", + message: errorMessage(err), + variant: "error", + }) + return undefined + } } export async function openWorkspaceSelect(input: { @@ -100,15 +102,23 @@ export async function warpWorkspaceSession(input: { copyChanges: boolean done?: () => void }): Promise { - const result = await input.sdk.client.experimental.workspace - .warp({ + let result + try { + result = await input.sdk.client.experimental.workspace.warp({ id: input.workspaceID, sessionID: input.sessionID, copyChanges: input.copyChanges, }) - .catch(() => undefined) + } catch (err) { + input.toast.show({ + title: "Failed to warp session", + message: errorMessage(err), + variant: "error", + }) + return false + } if (!result?.data) { - if (result?.error?.name === "VcsApplyError") { + if (result?.error && "name" in result.error && result.error.name === "VcsApplyError") { await DialogAlert.show( input.dialog, "Unable to Warp Session", @@ -118,7 +128,8 @@ export async function warpWorkspaceSession(input: { } input.toast.show({ - message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`, + title: "Failed to warp session", + message: errorMessage(result?.error ?? "no response"), variant: "error", }) return false diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx new file mode 100644 index 00000000000..3b36523f2b0 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx @@ -0,0 +1,112 @@ +import type { Workspace } from "@kilocode/sdk/v2" +import { useDialog } from "@tui/ui/dialog" +import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" +import { useProject } from "@tui/context/project" +import { useRoute } from "@tui/context/route" +import { useSync } from "@tui/context/sync" +import { useTheme } from "@tui/context/theme" +import { createMemo, createSignal, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { errorMessage } from "@/util/error" +import { useSDK } from "../context/sdk" +import { useToast } from "../ui/toast" + +type WorkspaceOption = { workspace: Workspace } + +export function DialogWorkspaceList() { + const dialog = useDialog() + const route = useRoute() + const sync = useSync() + const sdk = useSDK() + const toast = useToast() + const project = useProject() + const { theme } = useTheme() + const [deleting, setDeleting] = createSignal() + const [removing, setRemoving] = createSignal() + const [expanded, setExpanded] = createStore>({}) + + const current = createMemo(() => { + if (route.data.type === "session") return sync.session.get(route.data.sessionID)?.workspaceID + return project.workspace.current() + }) + + const options = createMemo[]>(() => + project.workspace + .list() + .toSorted((a, b) => a.name.localeCompare(b.name)) + .map((workspace) => { + const status = project.workspace.status(workspace.id) + return { + title: + removing() === workspace.id + ? "Deleting..." + : deleting() === workspace.id + ? `Delete ${workspace.name}? Press delete again` + : workspace.name, + value: { workspace }, + footer: workspace.type, + details: expanded[workspace.id] && workspace.directory ? [workspace.directory] : undefined, + gutter: () => , + } + }), + ) + + function showDetails(workspace: Workspace) { + setExpanded(workspace.id, (open) => !open) + } + + async function remove(workspace: Workspace) { + if (removing()) return + if (deleting() !== workspace.id) { + setDeleting(workspace.id) + return + } + + setDeleting(undefined) + setRemoving(workspace.id) + const result = await sdk.client.experimental.workspace.remove({ id: workspace.id }).catch((err) => ({ + error: err, + })) + if (result?.error) { + setRemoving(undefined) + toast.show({ + variant: "error", + title: "Failed to delete workspace", + message: errorMessage(result.error), + }) + return + } + + if (current() === workspace.id) { + project.workspace.set(undefined) + route.navigate({ type: "home" }) + } + await project.workspace.sync() + await sync.bootstrap({ fatal: false }).catch(() => undefined) + setRemoving(undefined) + } + + onMount(() => { + dialog.setSize("large") + void sdk.client.experimental.workspace.syncList().catch(() => undefined) + void project.workspace.sync() + }) + + return ( + { + setDeleting(undefined) + }} + onSelect={(option) => showDetails(option.value.workspace)} + actions={[ + { + command: "session.delete", + title: "delete", + onTrigger: (option) => void remove(option.value.workspace), + }, + ]} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx index 8d7fe968802..18411999087 100644 --- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx @@ -1,34 +1,41 @@ import { TextAttributes } from "@opentui/core" -import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" +import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import { createSignal } from "solid-js" import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { win32FlushInputBuffer } from "../win32" import { getScrollAcceleration } from "../util/scroll" +// kilocode_change start — guard against missing renderer context in ErrorBoundary fallback +function tryUseTerminalDimensions() { + try { + return useTerminalDimensions() + } catch { + return undefined + } +} +// kilocode_change end + export function ErrorComponent(props: { error: Error reset: () => void - onBeforeExit?: () => Promise - onExit: () => Promise + exit: () => Promise mode?: "dark" | "light" }) { - const term = useTerminalDimensions() - const renderer = useRenderer() + // kilocode_change start — guard against missing renderer context in ErrorBoundary fallback + const term = tryUseTerminalDimensions() + const height = () => term?.().height ?? process.stdout.rows ?? 24 - const handleExit = async () => { - await props.onBeforeExit?.() - renderer.setTerminalTitle("") - renderer.destroy() - win32FlushInputBuffer() - await props.onExit() + try { + useKeyboard((evt) => { + if (evt.ctrl && evt.name === "c") { + void props.exit() + } + }) + } catch { + // Keyboard handler unavailable — renderer context may be missing. + // Ctrl+C will still work via the default signal handler. } - - useKeyboard((evt) => { - if (evt.ctrl && evt.name === "c") { - void handleExit() - } - }) + // kilocode_change end const [copied, setCopied] = createSignal(false) const issueURL = new URL("https://github.com/Kilo-Org/kilocode/issues/new?template=bug-report.yml") // kilocode_change @@ -53,7 +60,7 @@ export function ErrorComponent(props: { ) } - issueURL.searchParams.set("opencode-version", InstallationVersion) + issueURL.searchParams.set("kilo-version", InstallationVersion) // kilocode_change const copyIssueURL = () => { void Clipboard.copy(issueURL.toString()).then(() => { @@ -79,11 +86,14 @@ export function ErrorComponent(props: { Reset TUI - + void props.exit()} backgroundColor={colors.primary} padding={1}> Exit - + {props.error.stack} {props.error.message} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 0999ed0537d..920d925deb5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -6,21 +6,21 @@ import { firstBy } from "remeda" import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { useEditorContext } from "@tui/context/editor" +import { useProject } from "@tui/context/project" import { useSDK } from "@tui/context/sdk" import { useSync } from "@tui/context/sync" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" import { useTheme, selectedForeground } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" -import { useCommandPalette } from "../../context/command-palette" import { useTerminalDimensions } from "@opentui/solid" import { slashDisplay } from "@/kilocode/cli/cmd/command-display" // kilocode_change import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" -import { useBindings } from "../../keymap" +import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap" import { Reference } from "@/reference/reference" -import type { Config } from "@/config/config" +import { ConfigReference } from "@/config/reference" import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display" function removeLineRange(input: string) { @@ -90,7 +90,9 @@ export function Autocomplete(props: { const editor = useEditorContext() const sdk = useSDK() const sync = useSync() - const command = useCommandPalette() + const project = useProject() + const slashes = useCommandSlashes() + const modeStack = useOpencodeModeStack() const { theme } = useTheme() const dimensions = useTerminalDimensions() const frecency = useFrecency() @@ -104,6 +106,12 @@ export function Autocomplete(props: { const [positionTick, setPositionTick] = createSignal(0) + createEffect(() => { + if (!store.visible) return + const popMode = modeStack.push("autocomplete") + onCleanup(popMode) + }) + createEffect(() => { if (store.visible) { let lastPos = { x: 0, y: 0, width: 0 } @@ -315,7 +323,7 @@ export function Autocomplete(props: { `Referenced configured reference @${reference.name}.`, ...(reference.kind === "local" ? ["Kind: local directory"] : []), ...(reference.kind === "git" ? ["Kind: git repository"] : []), - ...(reference.kind === "invalid" ? [`Repository: ${reference.repository}`] : []), + ...(reference.kind === "invalid" && reference.repository ? [`Repository: ${reference.repository}`] : []), ...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []), ...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []), ...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]), @@ -329,7 +337,7 @@ export function Autocomplete(props: { const references = createMemo(() => Reference.resolveAll({ - references: (sync.data.config.reference ?? {}) as NonNullable, + references: ConfigReference.normalize(sync.data.config.reference ?? {}), directory: sync.path.directory || process.cwd(), worktree: sync.path.worktree || sync.path.directory || process.cwd(), }), @@ -370,7 +378,6 @@ export function Autocomplete(props: { const { filename, part } = createFilePart(item, lineRange) const index = store.visible === "@" ? store.index : props.input().cursorOffset - command.suspend(false) setStore("visible", false) setStore("index", index) insertPart(filename, part) @@ -387,6 +394,7 @@ export function Autocomplete(props: { // Get files from SDK const result = await sdk.client.find.files({ query: baseQuery, + workspace: project.workspace.current(), }) const options: AutocompleteOption[] = [] @@ -541,7 +549,7 @@ export function Autocomplete(props: { ) const commands = createMemo((): AutocompleteOption[] => { - const results: AutocompleteOption[] = [...command.slashes()] + const results: AutocompleteOption[] = [...slashes()] for (const serverCommand of sync.data.command) { // kilocode_change start - preserve suffixes like :skill when inserting selected slash commands @@ -744,7 +752,6 @@ export function Autocomplete(props: { })) function show(mode: "@" | "/") { - command.suspend(true) setStore({ visible: mode, index: props.input().cursorOffset, @@ -755,9 +762,9 @@ export function Autocomplete(props: { // but still allow normal autocomplete dismissal to clean it up. function dismiss() { if (!store.visible) return - command.suspend(false) setStore("visible", false) } + // kilocode_change end function hide() { const text = props.input().plainText @@ -769,9 +776,8 @@ export function Autocomplete(props: { draft.input = props.input().plainText }) } - dismiss() + setStore("visible", false) } - // kilocode_change end onMount(() => { const unsubscribeMention = editor.onMention((mention) => { diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 476149cfe59..325e70a9cc8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -28,7 +28,7 @@ import { MessageID, PartID } from "@/session/schema" import { createStore, produce, unwrap } from "solid-js/store" import { usePromptHistory, type PromptInfo } from "./history" import { computePromptTraits } from "./traits" -import { assign } from "./part" +import { assign, expandPastedTextPlaceholders } from "./part" import { usePromptStash } from "./stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" @@ -40,6 +40,7 @@ import type { AssistantMessage, FilePart, UserMessage } from "@kilocode/sdk/v2" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" +import { errorMessage } from "@/util/error" import { formatDuration } from "@/util/format" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" @@ -63,13 +64,11 @@ import { slashMatches } from "@/kilocode/cli/cmd/command-display" // kilocode_change end import { Flag } from "@opencode-ai/core/flag/flag" import { type WorkspaceStatus } from "../workspace-label" -import { useCommandPalette } from "../../context/command-palette" -import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap" +import { KILO_BASE_MODE, useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap" import { useTuiConfig } from "../../context/tui-config" export type PromptProps = { sessionID?: string - workspaceID?: string visible?: boolean disabled?: boolean onSubmit?: () => void @@ -156,7 +155,6 @@ export function Prompt(props: PromptProps) { const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" }) const history = usePromptHistory() const stash = usePromptStash() - const command = useCommandPalette() const keymap = useOpencodeKeymap() const agentShortcut = useCommandShortcut("agent.cycle") const paletteShortcut = useCommandShortcut("command.palette.show") @@ -207,7 +205,6 @@ export function Prompt(props: PromptProps) { const [cursorVersion, setCursorVersion] = createSignal(0) const currentProviderLabel = createMemo(() => local.model.parsed().provider) const hasRightContent = createMemo(() => Boolean(props.right)) - const defaultWorkspaceID = createMemo(() => props.workspaceID ?? project.workspace.current()) function selectWorkspace(selection: WorkspaceSelection | undefined) { setWorkspaceSelection(selection) @@ -224,14 +221,25 @@ export function Prompt(props: PromptProps) { async function createWorkspace(selection: Extract) { setCreatingWorkspace(true) - const result = await sdk.client.experimental.workspace - .create({ type: selection.workspaceType, branch: null }) - .catch(() => undefined) - if (result == undefined || result.error || !result.data) { + let result + try { + result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null }) + } catch (err) { selectWorkspace(undefined) setCreatingWorkspace(false) toast.show({ - message: "Creating workspace failed", + title: "Creating workspace failed", + message: errorMessage(err), + variant: "error", + }) + return + } + if (result.error || !result.data) { + selectWorkspace(undefined) + setCreatingWorkspace(false) + toast.show({ + title: "Creating workspace failed", + message: errorMessage(result.error ?? "no response"), variant: "error", }) return @@ -522,7 +530,14 @@ export function Prompt(props: PromptProps) { const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") const value = text - const content = await Editor.open({ value, renderer }) + const content = await Editor.open({ + value, + renderer, + cwd: + (project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) || + project.instance.directory() || + process.cwd(), + }) if (!content) return input.setText(content) @@ -638,7 +653,7 @@ export function Prompt(props: PromptProps) { })) useBindings(() => ({ - enabled: command.matcher, + mode: KILO_BASE_MODE, bindings: tuiConfig.keybinds.gather("prompt.palette", [ "prompt.submit", "prompt.editor", @@ -1101,7 +1116,7 @@ export function Prompt(props: PromptProps) { if (sessionID == null) { const workspace = workspaceSelection() const workspaceID = iife(() => { - if (!workspace) return defaultWorkspaceID() + if (!workspace) return undefined if (workspace.type === "none") return undefined if (workspace.type === "existing") return workspace.workspaceID return undefined @@ -1463,17 +1478,7 @@ export function Prompt(props: PromptProps) { | undefined >(() => { const selected = workspaceSelection() - if (!selected) { - const workspaceID = defaultWorkspaceID() - if (props.sessionID || !workspaceID) return - const workspace = project.workspace.get(workspaceID) - return { - type: "existing", - workspaceType: workspace?.type ?? "unknown", - workspaceName: workspace?.name ?? workspaceID, - status: project.workspace.status(workspaceID) ?? "error", - } - } + if (!selected) return if (selected.type === "none") return if (props.sessionID && !workspaceCreating()) return if (selected.type === "new") { @@ -1491,7 +1496,10 @@ export function Prompt(props: PromptProps) { }) const spinnerDef = createMemo(() => { - const agent = local.agent.current() + const agent = + status().type !== "idle" + ? (local.agent.list().find((a) => a.name === lastUserMessage()?.agent) ?? local.agent.current()) + : local.agent.current() const color = agent ? local.agent.color(agent.name ?? "") : theme.border // kilocode_change return { frames: createFrames({ @@ -1510,11 +1518,13 @@ export function Prompt(props: PromptProps) { }), } }) + const maxHeight = createMemo(() => tuiConfig.prompt?.max_height ?? Math.max(6, Math.floor(dimensions().height / 3))) return ( <> - (anchor = r)} visible={props.visible !== false}> + (anchor = r)} visible={props.visible !== false} width="100%">