@qduc/term2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent.d.ts +19 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +143 -0
- package/dist/agent.js.map +1 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +403 -0
- package/dist/app.js.map +1 -0
- package/dist/app.model-command-feedback.test.d.ts +2 -0
- package/dist/app.model-command-feedback.test.d.ts.map +1 -0
- package/dist/app.model-command-feedback.test.js +19 -0
- package/dist/app.model-command-feedback.test.js.map +1 -0
- package/dist/app.parseInput.test.d.ts +2 -0
- package/dist/app.parseInput.test.d.ts.map +1 -0
- package/dist/app.parseInput.test.js +97 -0
- package/dist/app.parseInput.test.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +241 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/ApprovalPrompt.d.ts +10 -0
- package/dist/components/ApprovalPrompt.d.ts.map +1 -0
- package/dist/components/ApprovalPrompt.js +163 -0
- package/dist/components/ApprovalPrompt.js.map +1 -0
- package/dist/components/Banner.d.ts +9 -0
- package/dist/components/Banner.d.ts.map +1 -0
- package/dist/components/Banner.js +86 -0
- package/dist/components/Banner.js.map +1 -0
- package/dist/components/BottomArea.d.ts +33 -0
- package/dist/components/BottomArea.d.ts.map +1 -0
- package/dist/components/BottomArea.js +31 -0
- package/dist/components/BottomArea.js.map +1 -0
- package/dist/components/BottomArea.test.d.ts +2 -0
- package/dist/components/BottomArea.test.d.ts.map +1 -0
- package/dist/components/BottomArea.test.js +73 -0
- package/dist/components/BottomArea.test.js.map +1 -0
- package/dist/components/ChatMessage.d.ts +7 -0
- package/dist/components/ChatMessage.d.ts.map +1 -0
- package/dist/components/ChatMessage.js +10 -0
- package/dist/components/ChatMessage.js.map +1 -0
- package/dist/components/CommandMessage.d.ts +15 -0
- package/dist/components/CommandMessage.d.ts.map +1 -0
- package/dist/components/CommandMessage.js +188 -0
- package/dist/components/CommandMessage.js.map +1 -0
- package/dist/components/CommandMessage.test.d.ts +2 -0
- package/dist/components/CommandMessage.test.d.ts.map +1 -0
- package/dist/components/CommandMessage.test.js +35 -0
- package/dist/components/CommandMessage.test.js.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +27 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +77 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ErrorBoundary.test.d.ts +2 -0
- package/dist/components/ErrorBoundary.test.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.test.js +32 -0
- package/dist/components/ErrorBoundary.test.js.map +1 -0
- package/dist/components/Input/PopupManager.d.ts +42 -0
- package/dist/components/Input/PopupManager.d.ts.map +1 -0
- package/dist/components/Input/PopupManager.js +13 -0
- package/dist/components/Input/PopupManager.js.map +1 -0
- package/dist/components/InputBox.d.ts +18 -0
- package/dist/components/InputBox.d.ts.map +1 -0
- package/dist/components/InputBox.js +384 -0
- package/dist/components/InputBox.js.map +1 -0
- package/dist/components/InputBox.menu-logic.test.d.ts +2 -0
- package/dist/components/InputBox.menu-logic.test.d.ts.map +1 -0
- package/dist/components/InputBox.menu-logic.test.js +151 -0
- package/dist/components/InputBox.menu-logic.test.js.map +1 -0
- package/dist/components/InputBox.test.d.ts +2 -0
- package/dist/components/InputBox.test.d.ts.map +1 -0
- package/dist/components/InputBox.test.js +91 -0
- package/dist/components/InputBox.test.js.map +1 -0
- package/dist/components/LiveResponse.d.ts +13 -0
- package/dist/components/LiveResponse.d.ts.map +1 -0
- package/dist/components/LiveResponse.js +16 -0
- package/dist/components/LiveResponse.js.map +1 -0
- package/dist/components/MarkdownRenderer.d.ts +8 -0
- package/dist/components/MarkdownRenderer.d.ts.map +1 -0
- package/dist/components/MarkdownRenderer.js +225 -0
- package/dist/components/MarkdownRenderer.js.map +1 -0
- package/dist/components/MentorMode.test.d.ts +2 -0
- package/dist/components/MentorMode.test.d.ts.map +1 -0
- package/dist/components/MentorMode.test.js.map +1 -0
- package/dist/components/MessageList.d.ts +7 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.js +29 -0
- package/dist/components/MessageList.js.map +1 -0
- package/dist/components/MessageList.test.d.ts +2 -0
- package/dist/components/MessageList.test.d.ts.map +1 -0
- package/dist/components/MessageList.test.js +15 -0
- package/dist/components/MessageList.test.js.map +1 -0
- package/dist/components/ModelSelectionMenu.d.ts +18 -0
- package/dist/components/ModelSelectionMenu.d.ts.map +1 -0
- package/dist/components/ModelSelectionMenu.js +91 -0
- package/dist/components/ModelSelectionMenu.js.map +1 -0
- package/dist/components/ModelSelectionMenu.test.d.ts +2 -0
- package/dist/components/ModelSelectionMenu.test.d.ts.map +1 -0
- package/dist/components/ModelSelectionMenu.test.js +83 -0
- package/dist/components/ModelSelectionMenu.test.js.map +1 -0
- package/dist/components/PathSelectionMenu.d.ts +12 -0
- package/dist/components/PathSelectionMenu.d.ts.map +1 -0
- package/dist/components/PathSelectionMenu.js +42 -0
- package/dist/components/PathSelectionMenu.js.map +1 -0
- package/dist/components/SettingsSelectionMenu.d.ts +9 -0
- package/dist/components/SettingsSelectionMenu.d.ts.map +1 -0
- package/dist/components/SettingsSelectionMenu.js +21 -0
- package/dist/components/SettingsSelectionMenu.js.map +1 -0
- package/dist/components/SlashCommandMenu.d.ts +15 -0
- package/dist/components/SlashCommandMenu.d.ts.map +1 -0
- package/dist/components/SlashCommandMenu.js +20 -0
- package/dist/components/SlashCommandMenu.js.map +1 -0
- package/dist/components/StatusBar.d.ts +11 -0
- package/dist/components/StatusBar.d.ts.map +1 -0
- package/dist/components/StatusBar.js +59 -0
- package/dist/components/StatusBar.js.map +1 -0
- package/dist/components/TextInput.d.ts +42 -0
- package/dist/components/TextInput.d.ts.map +1 -0
- package/dist/components/TextInput.js +397 -0
- package/dist/components/TextInput.js.map +1 -0
- package/dist/components/TextInput.test.d.ts +2 -0
- package/dist/components/TextInput.test.d.ts.map +1 -0
- package/dist/components/TextInput.test.js +75 -0
- package/dist/components/TextInput.test.js.map +1 -0
- package/dist/context/InputContext.d.ts +31 -0
- package/dist/context/InputContext.d.ts.map +1 -0
- package/dist/context/InputContext.js +36 -0
- package/dist/context/InputContext.js.map +1 -0
- package/dist/context/InputContext.stability.test.d.ts +2 -0
- package/dist/context/InputContext.stability.test.d.ts.map +1 -0
- package/dist/context/InputContext.stability.test.js +28 -0
- package/dist/context/InputContext.stability.test.js.map +1 -0
- package/dist/context/InputContext.test.d.ts +2 -0
- package/dist/context/InputContext.test.d.ts.map +1 -0
- package/dist/context/InputContext.test.js +168 -0
- package/dist/context/InputContext.test.js.map +1 -0
- package/dist/debug-schema.d.ts +2 -0
- package/dist/debug-schema.d.ts.map +1 -0
- package/dist/debug-schema.js +22 -0
- package/dist/debug-schema.js.map +1 -0
- package/dist/hooks/use-conversation.d.ts +78 -0
- package/dist/hooks/use-conversation.d.ts.map +1 -0
- package/dist/hooks/use-conversation.js +1017 -0
- package/dist/hooks/use-conversation.js.map +1 -0
- package/dist/hooks/use-input-history.d.ts +16 -0
- package/dist/hooks/use-input-history.d.ts.map +1 -0
- package/dist/hooks/use-input-history.js +71 -0
- package/dist/hooks/use-input-history.js.map +1 -0
- package/dist/hooks/use-model-selection.d.ts +27 -0
- package/dist/hooks/use-model-selection.d.ts.map +1 -0
- package/dist/hooks/use-model-selection.js +187 -0
- package/dist/hooks/use-model-selection.js.map +1 -0
- package/dist/hooks/use-model-selection.test.d.ts +2 -0
- package/dist/hooks/use-model-selection.test.d.ts.map +1 -0
- package/dist/hooks/use-model-selection.test.js +28 -0
- package/dist/hooks/use-model-selection.test.js.map +1 -0
- package/dist/hooks/use-path-completion.d.ts +22 -0
- package/dist/hooks/use-path-completion.d.ts.map +1 -0
- package/dist/hooks/use-path-completion.js +153 -0
- package/dist/hooks/use-path-completion.js.map +1 -0
- package/dist/hooks/use-path-completion.test.d.ts +2 -0
- package/dist/hooks/use-path-completion.test.d.ts.map +1 -0
- package/dist/hooks/use-path-completion.test.js +29 -0
- package/dist/hooks/use-path-completion.test.js.map +1 -0
- package/dist/hooks/use-setting.d.ts +7 -0
- package/dist/hooks/use-setting.d.ts.map +1 -0
- package/dist/hooks/use-setting.js +35 -0
- package/dist/hooks/use-setting.js.map +1 -0
- package/dist/hooks/use-settings-completion.d.ts +23 -0
- package/dist/hooks/use-settings-completion.d.ts.map +1 -0
- package/dist/hooks/use-settings-completion.js +164 -0
- package/dist/hooks/use-settings-completion.js.map +1 -0
- package/dist/hooks/use-settings-completion.test.d.ts +2 -0
- package/dist/hooks/use-settings-completion.test.d.ts.map +1 -0
- package/dist/hooks/use-settings-completion.test.js +334 -0
- package/dist/hooks/use-settings-completion.test.js.map +1 -0
- package/dist/hooks/use-slash-commands.d.ts +21 -0
- package/dist/hooks/use-slash-commands.d.ts.map +1 -0
- package/dist/hooks/use-slash-commands.js +87 -0
- package/dist/hooks/use-slash-commands.js.map +1 -0
- package/dist/hooks/use-slash-commands.test.d.ts +2 -0
- package/dist/hooks/use-slash-commands.test.d.ts.map +1 -0
- package/dist/hooks/use-slash-commands.test.js +246 -0
- package/dist/hooks/use-slash-commands.test.js.map +1 -0
- package/dist/lib/editor-impl.d.ts +23 -0
- package/dist/lib/editor-impl.d.ts.map +1 -0
- package/dist/lib/editor-impl.js +235 -0
- package/dist/lib/editor-impl.js.map +1 -0
- package/dist/lib/openai-agent-client.chat.test.d.ts +2 -0
- package/dist/lib/openai-agent-client.chat.test.d.ts.map +1 -0
- package/dist/lib/openai-agent-client.chat.test.js +68 -0
- package/dist/lib/openai-agent-client.chat.test.js.map +1 -0
- package/dist/lib/openai-agent-client.d.ts +48 -0
- package/dist/lib/openai-agent-client.d.ts.map +1 -0
- package/dist/lib/openai-agent-client.js +653 -0
- package/dist/lib/openai-agent-client.js.map +1 -0
- package/dist/lib/openai-agent-client.test.d.ts +2 -0
- package/dist/lib/openai-agent-client.test.d.ts.map +1 -0
- package/dist/lib/openai-agent-client.test.js +181 -0
- package/dist/lib/openai-agent-client.test.js.map +1 -0
- package/dist/lib/shell.d.ts +7 -0
- package/dist/lib/shell.d.ts.map +1 -0
- package/dist/lib/shell.js +56 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/lib/tool-invoke.d.ts +4 -0
- package/dist/lib/tool-invoke.d.ts.map +1 -0
- package/dist/lib/tool-invoke.js +26 -0
- package/dist/lib/tool-invoke.js.map +1 -0
- package/dist/lib/tool-invoke.test.d.ts +2 -0
- package/dist/lib/tool-invoke.test.d.ts.map +1 -0
- package/dist/lib/tool-invoke.test.js +19 -0
- package/dist/lib/tool-invoke.test.js.map +1 -0
- package/dist/no-singleton-imports.test.d.ts +2 -0
- package/dist/no-singleton-imports.test.d.ts.map +1 -0
- package/dist/no-singleton-imports.test.js +30 -0
- package/dist/no-singleton-imports.test.js.map +1 -0
- package/dist/prompts/anthropic.md +79 -0
- package/dist/prompts/codex.md +97 -0
- package/dist/prompts/default.md +77 -0
- package/dist/prompts/default.md.bak +77 -0
- package/dist/prompts/gpt-5.md +318 -0
- package/dist/prompts/lite.md +29 -0
- package/dist/prompts/simple-mentor.md +207 -0
- package/dist/prompts/simple.md +189 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai-compatible/api.d.ts +17 -0
- package/dist/providers/openai-compatible/api.d.ts.map +1 -0
- package/dist/providers/openai-compatible/api.js +58 -0
- package/dist/providers/openai-compatible/api.js.map +1 -0
- package/dist/providers/openai-compatible/model.d.ts +17 -0
- package/dist/providers/openai-compatible/model.d.ts.map +1 -0
- package/dist/providers/openai-compatible/model.js +435 -0
- package/dist/providers/openai-compatible/model.js.map +1 -0
- package/dist/providers/openai-compatible/provider.d.ts +22 -0
- package/dist/providers/openai-compatible/provider.d.ts.map +1 -0
- package/dist/providers/openai-compatible/provider.js +43 -0
- package/dist/providers/openai-compatible/provider.js.map +1 -0
- package/dist/providers/openai-compatible/utils.d.ts +3 -0
- package/dist/providers/openai-compatible/utils.d.ts.map +1 -0
- package/dist/providers/openai-compatible/utils.js +11 -0
- package/dist/providers/openai-compatible/utils.js.map +1 -0
- package/dist/providers/openai-compatible.provider.d.ts +8 -0
- package/dist/providers/openai-compatible.provider.d.ts.map +1 -0
- package/dist/providers/openai-compatible.provider.js +71 -0
- package/dist/providers/openai-compatible.provider.js.map +1 -0
- package/dist/providers/openai.provider.d.ts +2 -0
- package/dist/providers/openai.provider.d.ts.map +1 -0
- package/dist/providers/openai.provider.js +36 -0
- package/dist/providers/openai.provider.js.map +1 -0
- package/dist/providers/openrouter/api.d.ts +39 -0
- package/dist/providers/openrouter/api.d.ts.map +1 -0
- package/dist/providers/openrouter/api.js +172 -0
- package/dist/providers/openrouter/api.js.map +1 -0
- package/dist/providers/openrouter/converters.d.ts +8 -0
- package/dist/providers/openrouter/converters.d.ts.map +1 -0
- package/dist/providers/openrouter/converters.js +382 -0
- package/dist/providers/openrouter/converters.js.map +1 -0
- package/dist/providers/openrouter/converters.test.d.ts +2 -0
- package/dist/providers/openrouter/converters.test.d.ts.map +1 -0
- package/dist/providers/openrouter/converters.test.js +158 -0
- package/dist/providers/openrouter/converters.test.js.map +1 -0
- package/dist/providers/openrouter/index.d.ts +4 -0
- package/dist/providers/openrouter/index.d.ts.map +1 -0
- package/dist/providers/openrouter/index.js +4 -0
- package/dist/providers/openrouter/index.js.map +1 -0
- package/dist/providers/openrouter/model.d.ts +14 -0
- package/dist/providers/openrouter/model.d.ts.map +1 -0
- package/dist/providers/openrouter/model.js +485 -0
- package/dist/providers/openrouter/model.js.map +1 -0
- package/dist/providers/openrouter/provider.d.ts +15 -0
- package/dist/providers/openrouter/provider.d.ts.map +1 -0
- package/dist/providers/openrouter/provider.js +21 -0
- package/dist/providers/openrouter/provider.js.map +1 -0
- package/dist/providers/openrouter/utils.d.ts +10 -0
- package/dist/providers/openrouter/utils.d.ts.map +1 -0
- package/dist/providers/openrouter/utils.js +27 -0
- package/dist/providers/openrouter/utils.js.map +1 -0
- package/dist/providers/openrouter.api.retry.test.d.ts +2 -0
- package/dist/providers/openrouter.api.retry.test.d.ts.map +1 -0
- package/dist/providers/openrouter.api.retry.test.js +148 -0
- package/dist/providers/openrouter.api.retry.test.js.map +1 -0
- package/dist/providers/openrouter.d.ts +2 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.history.test.d.ts +2 -0
- package/dist/providers/openrouter.history.test.d.ts.map +1 -0
- package/dist/providers/openrouter.history.test.js +533 -0
- package/dist/providers/openrouter.history.test.js.map +1 -0
- package/dist/providers/openrouter.js +4 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/openrouter.provider.createRunner.test.d.ts +2 -0
- package/dist/providers/openrouter.provider.createRunner.test.d.ts.map +1 -0
- package/dist/providers/openrouter.provider.createRunner.test.js +23 -0
- package/dist/providers/openrouter.provider.createRunner.test.js.map +1 -0
- package/dist/providers/openrouter.provider.d.ts +2 -0
- package/dist/providers/openrouter.provider.d.ts.map +1 -0
- package/dist/providers/openrouter.provider.js +56 -0
- package/dist/providers/openrouter.provider.js.map +1 -0
- package/dist/providers/openrouter.test.d.ts +2 -0
- package/dist/providers/openrouter.test.d.ts.map +1 -0
- package/dist/providers/openrouter.test.js +1382 -0
- package/dist/providers/openrouter.test.js.map +1 -0
- package/dist/providers/registry.d.ts +65 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +44 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/registry.test.d.ts +2 -0
- package/dist/providers/registry.test.d.ts.map +1 -0
- package/dist/providers/registry.test.js +76 -0
- package/dist/providers/registry.test.js.map +1 -0
- package/dist/providers/web-search/index.d.ts +8 -0
- package/dist/providers/web-search/index.d.ts.map +1 -0
- package/dist/providers/web-search/index.js +9 -0
- package/dist/providers/web-search/index.js.map +1 -0
- package/dist/providers/web-search/registry.d.ts +35 -0
- package/dist/providers/web-search/registry.d.ts.map +1 -0
- package/dist/providers/web-search/registry.js +56 -0
- package/dist/providers/web-search/registry.js.map +1 -0
- package/dist/providers/web-search/registry.test.d.ts +2 -0
- package/dist/providers/web-search/registry.test.d.ts.map +1 -0
- package/dist/providers/web-search/registry.test.js +105 -0
- package/dist/providers/web-search/registry.test.js.map +1 -0
- package/dist/providers/web-search/tavily.provider.d.ts +15 -0
- package/dist/providers/web-search/tavily.provider.d.ts.map +1 -0
- package/dist/providers/web-search/tavily.provider.js +69 -0
- package/dist/providers/web-search/tavily.provider.js.map +1 -0
- package/dist/providers/web-search/tavily.provider.test.d.ts +2 -0
- package/dist/providers/web-search/tavily.provider.test.d.ts.map +1 -0
- package/dist/providers/web-search/tavily.provider.test.js +67 -0
- package/dist/providers/web-search/tavily.provider.test.js.map +1 -0
- package/dist/providers/web-search/types.d.ts +55 -0
- package/dist/providers/web-search/types.d.ts.map +1 -0
- package/dist/providers/web-search/types.js +6 -0
- package/dist/providers/web-search/types.js.map +1 -0
- package/dist/safety-checker.js +57 -0
- package/dist/services/conversation-events.d.ts +76 -0
- package/dist/services/conversation-events.d.ts.map +1 -0
- package/dist/services/conversation-events.js +2 -0
- package/dist/services/conversation-events.js.map +1 -0
- package/dist/services/conversation-service.d.ts +31 -0
- package/dist/services/conversation-service.d.ts.map +1 -0
- package/dist/services/conversation-service.js +46 -0
- package/dist/services/conversation-service.js.map +1 -0
- package/dist/services/conversation-service.test.js +190 -0
- package/dist/services/conversation-session.d.ts +99 -0
- package/dist/services/conversation-session.d.ts.map +1 -0
- package/dist/services/conversation-session.js +978 -0
- package/dist/services/conversation-session.js.map +1 -0
- package/dist/services/conversation-store.d.ts +24 -0
- package/dist/services/conversation-store.d.ts.map +1 -0
- package/dist/services/conversation-store.js +216 -0
- package/dist/services/conversation-store.js.map +1 -0
- package/dist/services/conversation-store.test.d.ts +2 -0
- package/dist/services/conversation-store.test.d.ts.map +1 -0
- package/dist/services/conversation-store.test.js +167 -0
- package/dist/services/conversation-store.test.js.map +1 -0
- package/dist/services/execution-context.d.ts +10 -0
- package/dist/services/execution-context.d.ts.map +1 -0
- package/dist/services/execution-context.js +22 -0
- package/dist/services/execution-context.js.map +1 -0
- package/dist/services/execution-context.test.d.ts +2 -0
- package/dist/services/execution-context.test.d.ts.map +1 -0
- package/dist/services/execution-context.test.js +49 -0
- package/dist/services/execution-context.test.js.map +1 -0
- package/dist/services/file-service.d.ts +12 -0
- package/dist/services/file-service.d.ts.map +1 -0
- package/dist/services/file-service.js +90 -0
- package/dist/services/file-service.js.map +1 -0
- package/dist/services/history-service.d.ts +39 -0
- package/dist/services/history-service.d.ts.map +1 -0
- package/dist/services/history-service.js +152 -0
- package/dist/services/history-service.js.map +1 -0
- package/dist/services/logging-service.d.ts +75 -0
- package/dist/services/logging-service.d.ts.map +1 -0
- package/dist/services/logging-service.js +343 -0
- package/dist/services/logging-service.js.map +1 -0
- package/dist/services/model-service.d.ts +15 -0
- package/dist/services/model-service.d.ts.map +1 -0
- package/dist/services/model-service.js +46 -0
- package/dist/services/model-service.js.map +1 -0
- package/dist/services/model-service.test.d.ts +2 -0
- package/dist/services/model-service.test.d.ts.map +1 -0
- package/dist/services/model-service.test.js +128 -0
- package/dist/services/model-service.test.js.map +1 -0
- package/dist/services/service-interfaces.d.ts +33 -0
- package/dist/services/service-interfaces.d.ts.map +1 -0
- package/dist/services/service-interfaces.js +2 -0
- package/dist/services/service-interfaces.js.map +1 -0
- package/dist/services/settings-service.d.ts +316 -0
- package/dist/services/settings-service.d.ts.map +1 -0
- package/dist/services/settings-service.js +1128 -0
- package/dist/services/settings-service.js.map +1 -0
- package/dist/services/settings-service.mock.d.ts +20 -0
- package/dist/services/settings-service.mock.d.ts.map +1 -0
- package/dist/services/settings-service.mock.js +55 -0
- package/dist/services/settings-service.mock.js.map +1 -0
- package/dist/services/singleton-deprecation.test.d.ts +2 -0
- package/dist/services/singleton-deprecation.test.d.ts.map +1 -0
- package/dist/services/singleton-deprecation.test.js +59 -0
- package/dist/services/singleton-deprecation.test.js.map +1 -0
- package/dist/services/ssh-service.d.ts +32 -0
- package/dist/services/ssh-service.d.ts.map +1 -0
- package/dist/services/ssh-service.js +119 -0
- package/dist/services/ssh-service.js.map +1 -0
- package/dist/services/ssh-service.test.d.ts +2 -0
- package/dist/services/ssh-service.test.d.ts.map +1 -0
- package/dist/services/ssh-service.test.js +269 -0
- package/dist/services/ssh-service.test.js.map +1 -0
- package/dist/test-search-tool.d.ts +2 -0
- package/dist/test-search-tool.d.ts.map +1 -0
- package/dist/test-search-tool.js +36 -0
- package/dist/test-search-tool.js.map +1 -0
- package/dist/tools/apply-patch.d.ts +28 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/apply-patch.js +399 -0
- package/dist/tools/apply-patch.js.map +1 -0
- package/dist/tools/apply-patch.test.d.ts +2 -0
- package/dist/tools/apply-patch.test.d.ts.map +1 -0
- package/dist/tools/apply-patch.test.js +155 -0
- package/dist/tools/apply-patch.test.js.map +1 -0
- package/dist/tools/ask-mentor.d.ts +11 -0
- package/dist/tools/ask-mentor.d.ts.map +1 -0
- package/dist/tools/ask-mentor.js +52 -0
- package/dist/tools/ask-mentor.js.map +1 -0
- package/dist/tools/ask-mentor.test.d.ts +2 -0
- package/dist/tools/ask-mentor.test.d.ts.map +1 -0
- package/dist/tools/ask-mentor.test.js +47 -0
- package/dist/tools/ask-mentor.test.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +55 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/find-files.d.ts +15 -0
- package/dist/tools/find-files.d.ts.map +1 -0
- package/dist/tools/find-files.js +179 -0
- package/dist/tools/find-files.js.map +1 -0
- package/dist/tools/find-files.test.d.ts +2 -0
- package/dist/tools/find-files.test.d.ts.map +1 -0
- package/dist/tools/find-files.test.js +131 -0
- package/dist/tools/find-files.test.js.map +1 -0
- package/dist/tools/format-helpers.d.ts +34 -0
- package/dist/tools/format-helpers.d.ts.map +1 -0
- package/dist/tools/format-helpers.js +131 -0
- package/dist/tools/format-helpers.js.map +1 -0
- package/dist/tools/grep.d.ts +16 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +211 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/read-file.d.ts +15 -0
- package/dist/tools/read-file.d.ts.map +1 -0
- package/dist/tools/read-file.js +114 -0
- package/dist/tools/read-file.js.map +1 -0
- package/dist/tools/read-file.test.d.ts +2 -0
- package/dist/tools/read-file.test.d.ts.map +1 -0
- package/dist/tools/read-file.test.js +122 -0
- package/dist/tools/read-file.test.js.map +1 -0
- package/dist/tools/search-replace.d.ts +19 -0
- package/dist/tools/search-replace.d.ts.map +1 -0
- package/dist/tools/search-replace.js +411 -0
- package/dist/tools/search-replace.js.map +1 -0
- package/dist/tools/search-replace.test.d.ts +2 -0
- package/dist/tools/search-replace.test.d.ts.map +1 -0
- package/dist/tools/search-replace.test.js +302 -0
- package/dist/tools/search-replace.test.js.map +1 -0
- package/dist/tools/search.d.ts +15 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +143 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shell.d.ts +19 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +278 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/tools/tool-execution-context.d.ts +7 -0
- package/dist/tools/tool-execution-context.d.ts.map +1 -0
- package/dist/tools/tool-execution-context.js +7 -0
- package/dist/tools/tool-execution-context.js.map +1 -0
- package/dist/tools/types.d.ts +30 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/utils.d.ts +12 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +19 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/web-search.d.ts +29 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +106 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tools/web-search.test.d.ts +2 -0
- package/dist/tools/web-search.test.d.ts.map +1 -0
- package/dist/tools/web-search.test.js +176 -0
- package/dist/tools/web-search.test.js.map +1 -0
- package/dist/utils/command-logger.d.ts +11 -0
- package/dist/utils/command-logger.d.ts.map +1 -0
- package/dist/utils/command-logger.js +34 -0
- package/dist/utils/command-logger.js.map +1 -0
- package/dist/utils/command-safety/constants.d.ts +21 -0
- package/dist/utils/command-safety/constants.d.ts.map +1 -0
- package/dist/utils/command-safety/constants.js +245 -0
- package/dist/utils/command-safety/constants.js.map +1 -0
- package/dist/utils/command-safety/find-helpers.d.ts +15 -0
- package/dist/utils/command-safety/find-helpers.d.ts.map +1 -0
- package/dist/utils/command-safety/find-helpers.js +218 -0
- package/dist/utils/command-safety/find-helpers.js.map +1 -0
- package/dist/utils/command-safety/handlers/find-handler.d.ts +6 -0
- package/dist/utils/command-safety/handlers/find-handler.d.ts.map +1 -0
- package/dist/utils/command-safety/handlers/find-handler.js +113 -0
- package/dist/utils/command-safety/handlers/find-handler.js.map +1 -0
- package/dist/utils/command-safety/handlers/git-handler.d.ts +6 -0
- package/dist/utils/command-safety/handlers/git-handler.d.ts.map +1 -0
- package/dist/utils/command-safety/handlers/git-handler.js +68 -0
- package/dist/utils/command-safety/handlers/git-handler.js.map +1 -0
- package/dist/utils/command-safety/handlers/index.d.ts +13 -0
- package/dist/utils/command-safety/handlers/index.d.ts.map +1 -0
- package/dist/utils/command-safety/handlers/index.js +20 -0
- package/dist/utils/command-safety/handlers/index.js.map +1 -0
- package/dist/utils/command-safety/handlers/sed-handler.d.ts +6 -0
- package/dist/utils/command-safety/handlers/sed-handler.d.ts.map +1 -0
- package/dist/utils/command-safety/handlers/sed-handler.js +94 -0
- package/dist/utils/command-safety/handlers/sed-handler.js.map +1 -0
- package/dist/utils/command-safety/handlers/types.d.ts +36 -0
- package/dist/utils/command-safety/handlers/types.d.ts.map +1 -0
- package/dist/utils/command-safety/handlers/types.js +2 -0
- package/dist/utils/command-safety/handlers/types.js.map +1 -0
- package/dist/utils/command-safety/index.d.ts +14 -0
- package/dist/utils/command-safety/index.d.ts.map +1 -0
- package/dist/utils/command-safety/index.js +183 -0
- package/dist/utils/command-safety/index.js.map +1 -0
- package/dist/utils/command-safety/path-analysis.d.ts +4 -0
- package/dist/utils/command-safety/path-analysis.d.ts.map +1 -0
- package/dist/utils/command-safety/path-analysis.js +153 -0
- package/dist/utils/command-safety/path-analysis.js.map +1 -0
- package/dist/utils/command-safety/utils.d.ts +2 -0
- package/dist/utils/command-safety/utils.d.ts.map +1 -0
- package/dist/utils/command-safety/utils.js +22 -0
- package/dist/utils/command-safety/utils.js.map +1 -0
- package/dist/utils/command-safety.d.ts +21 -0
- package/dist/utils/command-safety.d.ts.map +1 -0
- package/dist/utils/command-safety.find.test.d.ts +2 -0
- package/dist/utils/command-safety.find.test.d.ts.map +1 -0
- package/dist/utils/command-safety.find.test.js +342 -0
- package/dist/utils/command-safety.find.test.js.map +1 -0
- package/dist/utils/command-safety.js +702 -0
- package/dist/utils/command-safety.js.map +1 -0
- package/dist/utils/command-safety.path.test.d.ts +2 -0
- package/dist/utils/command-safety.path.test.d.ts.map +1 -0
- package/dist/utils/command-safety.path.test.js +360 -0
- package/dist/utils/command-safety.path.test.js.map +1 -0
- package/dist/utils/diff.d.ts +2 -0
- package/dist/utils/diff.d.ts.map +1 -0
- package/dist/utils/diff.js +44 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/diff.test.d.ts +2 -0
- package/dist/utils/diff.test.d.ts.map +1 -0
- package/dist/utils/diff.test.js +85 -0
- package/dist/utils/diff.test.js.map +1 -0
- package/dist/utils/error-helpers.d.ts +6 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +46 -0
- package/dist/utils/error-helpers.js.map +1 -0
- package/dist/utils/error-helpers.test.d.ts +2 -0
- package/dist/utils/error-helpers.test.d.ts.map +1 -0
- package/dist/utils/error-helpers.test.js +152 -0
- package/dist/utils/error-helpers.test.js.map +1 -0
- package/dist/utils/execute-shell.d.ts +15 -0
- package/dist/utils/execute-shell.d.ts.map +1 -0
- package/dist/utils/execute-shell.js +34 -0
- package/dist/utils/execute-shell.js.map +1 -0
- package/dist/utils/execute-shell.test.d.ts +2 -0
- package/dist/utils/execute-shell.test.d.ts.map +1 -0
- package/dist/utils/execute-shell.test.js +20 -0
- package/dist/utils/execute-shell.test.js.map +1 -0
- package/dist/utils/extract-command-messages.d.ts +5 -0
- package/dist/utils/extract-command-messages.d.ts.map +1 -0
- package/dist/utils/extract-command-messages.js +140 -0
- package/dist/utils/extract-command-messages.js.map +1 -0
- package/dist/utils/extract-command-messages.repro.test.d.ts +2 -0
- package/dist/utils/extract-command-messages.repro.test.d.ts.map +1 -0
- package/dist/utils/extract-command-messages.repro.test.js +31 -0
- package/dist/utils/extract-command-messages.repro.test.js.map +1 -0
- package/dist/utils/extract-command-messages.test.js +57 -0
- package/dist/utils/message-buffer.d.ts +2 -0
- package/dist/utils/message-buffer.d.ts.map +1 -0
- package/dist/utils/message-buffer.js +15 -0
- package/dist/utils/message-buffer.js.map +1 -0
- package/dist/utils/message-buffer.test.d.ts +2 -0
- package/dist/utils/message-buffer.test.d.ts.map +1 -0
- package/dist/utils/message-buffer.test.js +17 -0
- package/dist/utils/message-buffer.test.js.map +1 -0
- package/dist/utils/output-trim.d.ts +31 -0
- package/dist/utils/output-trim.d.ts.map +1 -0
- package/dist/utils/output-trim.js +71 -0
- package/dist/utils/output-trim.js.map +1 -0
- package/dist/utils/provider-credentials.d.ts +10 -0
- package/dist/utils/provider-credentials.d.ts.map +1 -0
- package/dist/utils/provider-credentials.js +22 -0
- package/dist/utils/provider-credentials.js.map +1 -0
- package/dist/utils/settings-command.d.ts +13 -0
- package/dist/utils/settings-command.d.ts.map +1 -0
- package/dist/utils/settings-command.js +173 -0
- package/dist/utils/settings-command.js.map +1 -0
- package/dist/utils/ssh-config-parser.d.ts +21 -0
- package/dist/utils/ssh-config-parser.d.ts.map +1 -0
- package/dist/utils/ssh-config-parser.js +89 -0
- package/dist/utils/ssh-config-parser.js.map +1 -0
- package/dist/utils/ssh-config-parser.test.d.ts +2 -0
- package/dist/utils/ssh-config-parser.test.d.ts.map +1 -0
- package/dist/utils/ssh-config-parser.test.js +153 -0
- package/dist/utils/ssh-config-parser.test.js.map +1 -0
- package/dist/utils/streaming-updater.d.ts +7 -0
- package/dist/utils/streaming-updater.d.ts.map +1 -0
- package/dist/utils/streaming-updater.js +41 -0
- package/dist/utils/streaming-updater.js.map +1 -0
- package/dist/utils/throttle.d.ts +7 -0
- package/dist/utils/throttle.d.ts.map +1 -0
- package/dist/utils/throttle.js +49 -0
- package/dist/utils/throttle.js.map +1 -0
- package/package.json +108 -0
- package/readme.md +428 -0
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
import { OpenRouterModel, OpenRouterError } from './openrouter.js';
|
|
4
|
+
import { createMockSettingsService } from '../services/settings-service.mock.js';
|
|
5
|
+
import { LoggingService } from '../services/logging-service.js';
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
// Use a dedicated logger instance for this test suite (avoid deprecated singleton)
|
|
8
|
+
const logger = new LoggingService({ disableLogging: true });
|
|
9
|
+
const originalLogToOpenrouter = logger.logToOpenrouter.bind(logger);
|
|
10
|
+
// Create a mock settings service with OpenRouter API key for tests
|
|
11
|
+
const mockSettingsService = createMockSettingsService({
|
|
12
|
+
agent: {
|
|
13
|
+
openrouter: {
|
|
14
|
+
apiKey: 'mock-api-key',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const createJsonResponse = (body) => new Response(JSON.stringify(body), {
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
test.beforeEach(() => {
|
|
23
|
+
// Nothing to do - mock is already configured
|
|
24
|
+
});
|
|
25
|
+
test.afterEach.always(() => {
|
|
26
|
+
globalThis.fetch = originalFetch;
|
|
27
|
+
logger.logToOpenrouter = originalLogToOpenrouter;
|
|
28
|
+
});
|
|
29
|
+
test.serial('builds messages from explicit history, tool calls, and reasoning config', async (t) => {
|
|
30
|
+
const requests = [];
|
|
31
|
+
const responses = [
|
|
32
|
+
createJsonResponse({
|
|
33
|
+
id: 'resp-1',
|
|
34
|
+
choices: [
|
|
35
|
+
{
|
|
36
|
+
message: {
|
|
37
|
+
content: 'Hello back',
|
|
38
|
+
reasoning_details: [
|
|
39
|
+
{ type: 'text', text: 'thinking' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
usage: {
|
|
45
|
+
prompt_tokens: 1,
|
|
46
|
+
completion_tokens: 2,
|
|
47
|
+
total_tokens: 3,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
createJsonResponse({
|
|
51
|
+
id: 'resp-2',
|
|
52
|
+
choices: [{ message: { content: 'Second turn reply' } }],
|
|
53
|
+
usage: {
|
|
54
|
+
prompt_tokens: 2,
|
|
55
|
+
completion_tokens: 1,
|
|
56
|
+
total_tokens: 3,
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
60
|
+
globalThis.fetch = async (_url, options) => {
|
|
61
|
+
const body = JSON.parse(options.body);
|
|
62
|
+
requests.push(body);
|
|
63
|
+
return responses[requests.length - 1];
|
|
64
|
+
};
|
|
65
|
+
const model = new OpenRouterModel({
|
|
66
|
+
settingsService: mockSettingsService,
|
|
67
|
+
loggingService: logger,
|
|
68
|
+
modelId: 'mock-model',
|
|
69
|
+
});
|
|
70
|
+
await model.getResponse({
|
|
71
|
+
systemInstructions: 'system message',
|
|
72
|
+
input: 'First user turn',
|
|
73
|
+
modelSettings: { reasoningEffort: 'high' },
|
|
74
|
+
});
|
|
75
|
+
// Caller-managed history: include the prior user + assistant messages explicitly.
|
|
76
|
+
const secondTurnItems = [
|
|
77
|
+
{ type: 'message', role: 'user', content: 'First user turn' },
|
|
78
|
+
{
|
|
79
|
+
type: 'message',
|
|
80
|
+
role: 'assistant',
|
|
81
|
+
content: [{ type: 'output_text', text: 'Hello back' }],
|
|
82
|
+
status: 'completed',
|
|
83
|
+
reasoning_details: [{ type: 'text', text: 'thinking' }],
|
|
84
|
+
},
|
|
85
|
+
{ type: 'input_text', text: 'Second user turn' },
|
|
86
|
+
{
|
|
87
|
+
type: 'function_call',
|
|
88
|
+
id: 'call-123',
|
|
89
|
+
name: 'bash',
|
|
90
|
+
arguments: '{"cmd":"ls"}',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'function_call_result',
|
|
94
|
+
callId: 'call-123',
|
|
95
|
+
output: 'files\n',
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
await model.getResponse({
|
|
99
|
+
systemInstructions: 'system message',
|
|
100
|
+
input: secondTurnItems,
|
|
101
|
+
});
|
|
102
|
+
t.truthy(requests[0]);
|
|
103
|
+
t.deepEqual(requests[0].reasoning, { effort: 'high' });
|
|
104
|
+
t.deepEqual(requests[0].tools, []);
|
|
105
|
+
const secondRequest = requests[1];
|
|
106
|
+
t.truthy(secondRequest);
|
|
107
|
+
t.deepEqual(secondRequest.messages, [
|
|
108
|
+
{ role: 'system', content: 'system message' },
|
|
109
|
+
{ role: 'user', content: 'First user turn' },
|
|
110
|
+
{
|
|
111
|
+
role: 'assistant',
|
|
112
|
+
content: 'Hello back',
|
|
113
|
+
reasoning_details: [{ type: 'text', text: 'thinking' }],
|
|
114
|
+
},
|
|
115
|
+
{ role: 'user', content: 'Second user turn' },
|
|
116
|
+
{
|
|
117
|
+
role: 'assistant',
|
|
118
|
+
content: null,
|
|
119
|
+
tool_calls: [
|
|
120
|
+
{
|
|
121
|
+
id: 'call-123',
|
|
122
|
+
type: 'function',
|
|
123
|
+
function: { name: 'bash', arguments: '{"cmd":"ls"}' },
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{ role: 'tool', tool_call_id: 'call-123', content: 'files\n' },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
test.serial('preserves assistant reasoning_details when nested under rawItem in history', async (t) => {
|
|
131
|
+
const requests = [];
|
|
132
|
+
globalThis.fetch = async (_url, options) => {
|
|
133
|
+
const body = JSON.parse(options.body);
|
|
134
|
+
requests.push(body);
|
|
135
|
+
return new Response(JSON.stringify({
|
|
136
|
+
choices: [{ message: { content: 'ok' } }],
|
|
137
|
+
usage: {},
|
|
138
|
+
}), {
|
|
139
|
+
status: 200,
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const model = new OpenRouterModel({
|
|
144
|
+
settingsService: mockSettingsService,
|
|
145
|
+
loggingService: logger,
|
|
146
|
+
modelId: 'mock-model',
|
|
147
|
+
});
|
|
148
|
+
await model.getResponse({
|
|
149
|
+
systemInstructions: 'system message',
|
|
150
|
+
input: [
|
|
151
|
+
{ type: 'message', role: 'user', content: 'hi' },
|
|
152
|
+
{
|
|
153
|
+
rawItem: {
|
|
154
|
+
type: 'message',
|
|
155
|
+
role: 'assistant',
|
|
156
|
+
status: 'completed',
|
|
157
|
+
content: [{ type: 'output_text', text: 'Hello back' }],
|
|
158
|
+
reasoning_details: [{ type: 'text', text: 'thinking' }],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
t.truthy(requests[0]);
|
|
164
|
+
t.deepEqual(requests[0].messages, [
|
|
165
|
+
{ role: 'system', content: 'system message' },
|
|
166
|
+
{ role: 'user', content: 'hi' },
|
|
167
|
+
{
|
|
168
|
+
role: 'assistant',
|
|
169
|
+
content: 'Hello back',
|
|
170
|
+
reasoning_details: [{ type: 'text', text: 'thinking' }],
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
test.serial('preserves assistant reasoning (reasoning tokens) in history messages', async (t) => {
|
|
175
|
+
const requests = [];
|
|
176
|
+
globalThis.fetch = async (_url, options) => {
|
|
177
|
+
const body = JSON.parse(options.body);
|
|
178
|
+
requests.push(body);
|
|
179
|
+
return createJsonResponse({
|
|
180
|
+
id: 'resp-ok',
|
|
181
|
+
choices: [{ message: { content: 'ok' } }],
|
|
182
|
+
usage: {},
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const model = new OpenRouterModel({
|
|
186
|
+
settingsService: mockSettingsService,
|
|
187
|
+
loggingService: logger,
|
|
188
|
+
modelId: 'mock-model',
|
|
189
|
+
});
|
|
190
|
+
await model.getResponse({
|
|
191
|
+
systemInstructions: 'system message',
|
|
192
|
+
input: [
|
|
193
|
+
{ type: 'message', role: 'user', content: 'hi' },
|
|
194
|
+
{
|
|
195
|
+
type: 'message',
|
|
196
|
+
role: 'assistant',
|
|
197
|
+
status: 'completed',
|
|
198
|
+
content: [{ type: 'output_text', text: 'Hello back' }],
|
|
199
|
+
reasoning: 'Thinking privately...',
|
|
200
|
+
reasoning_details: [
|
|
201
|
+
{
|
|
202
|
+
type: 'reasoning.text',
|
|
203
|
+
text: 'step1',
|
|
204
|
+
format: null,
|
|
205
|
+
index: 0,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
t.truthy(requests[0]);
|
|
212
|
+
t.deepEqual(requests[0].messages, [
|
|
213
|
+
{ role: 'system', content: 'system message' },
|
|
214
|
+
{ role: 'user', content: 'hi' },
|
|
215
|
+
{
|
|
216
|
+
role: 'assistant',
|
|
217
|
+
content: 'Hello back',
|
|
218
|
+
reasoning: 'Thinking privately...',
|
|
219
|
+
reasoning_details: [
|
|
220
|
+
{
|
|
221
|
+
type: 'reasoning.text',
|
|
222
|
+
text: 'step1',
|
|
223
|
+
format: null,
|
|
224
|
+
index: 0,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
test.serial('replays reasoning_details alongside tool_calls when stored on function_call items', async (t) => {
|
|
231
|
+
const requests = [];
|
|
232
|
+
globalThis.fetch = async (_url, options) => {
|
|
233
|
+
const body = JSON.parse(options.body);
|
|
234
|
+
requests.push(body);
|
|
235
|
+
return createJsonResponse({
|
|
236
|
+
id: 'resp-ok',
|
|
237
|
+
choices: [{ message: { content: 'ok' } }],
|
|
238
|
+
usage: {},
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const model = new OpenRouterModel({
|
|
242
|
+
settingsService: mockSettingsService,
|
|
243
|
+
loggingService: logger,
|
|
244
|
+
modelId: 'mock-model',
|
|
245
|
+
});
|
|
246
|
+
await model.getResponse({
|
|
247
|
+
systemInstructions: 'system message',
|
|
248
|
+
input: [
|
|
249
|
+
{ type: 'message', role: 'user', content: 'hi' },
|
|
250
|
+
{
|
|
251
|
+
type: 'function_call',
|
|
252
|
+
id: 'call-123',
|
|
253
|
+
name: 'bash',
|
|
254
|
+
arguments: '{"cmd":"ls"}',
|
|
255
|
+
reasoning_details: [
|
|
256
|
+
{
|
|
257
|
+
type: 'reasoning.text',
|
|
258
|
+
text: 'tool planning',
|
|
259
|
+
format: 'anthropic-claude-v1',
|
|
260
|
+
index: 0,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
reasoning: 'I should run ls',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: 'function_call_result',
|
|
267
|
+
callId: 'call-123',
|
|
268
|
+
output: 'files\n',
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
const messages = requests[0].messages;
|
|
273
|
+
const assistantWithToolCalls = messages.find((m) => m.role === 'assistant' && Array.isArray(m.tool_calls));
|
|
274
|
+
t.truthy(assistantWithToolCalls);
|
|
275
|
+
t.deepEqual(assistantWithToolCalls.reasoning_details, [
|
|
276
|
+
{
|
|
277
|
+
type: 'reasoning.text',
|
|
278
|
+
text: 'tool planning',
|
|
279
|
+
format: 'anthropic-claude-v1',
|
|
280
|
+
index: 0,
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
t.is(assistantWithToolCalls.reasoning, 'I should run ls');
|
|
284
|
+
});
|
|
285
|
+
test.serial('reconstructs reasoning_details from standalone reasoning items before tool calls (Gemini requirement)', async (t) => {
|
|
286
|
+
const requests = [];
|
|
287
|
+
globalThis.fetch = async (_url, options) => {
|
|
288
|
+
const body = JSON.parse(options.body);
|
|
289
|
+
requests.push(body);
|
|
290
|
+
return createJsonResponse({
|
|
291
|
+
id: 'resp-ok',
|
|
292
|
+
choices: [{ message: { content: 'ok' } }],
|
|
293
|
+
usage: {},
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const model = new OpenRouterModel({
|
|
297
|
+
settingsService: mockSettingsService,
|
|
298
|
+
loggingService: logger,
|
|
299
|
+
modelId: 'mock-model',
|
|
300
|
+
});
|
|
301
|
+
const input = [
|
|
302
|
+
{
|
|
303
|
+
role: 'user',
|
|
304
|
+
type: 'message',
|
|
305
|
+
content: 'do I have uncommitted change',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'reasoning-1',
|
|
309
|
+
type: 'reasoning',
|
|
310
|
+
providerData: {
|
|
311
|
+
type: 'reasoning.text',
|
|
312
|
+
text: 'step1',
|
|
313
|
+
format: 'google-gemini-v1',
|
|
314
|
+
index: 0,
|
|
315
|
+
},
|
|
316
|
+
content: [{ type: 'input_text', text: 'step1' }],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: 'tool_shell_1',
|
|
320
|
+
type: 'reasoning',
|
|
321
|
+
providerData: {
|
|
322
|
+
id: 'tool_shell_1',
|
|
323
|
+
type: 'reasoning.encrypted',
|
|
324
|
+
data: 'ENC',
|
|
325
|
+
format: 'google-gemini-v1',
|
|
326
|
+
index: 1,
|
|
327
|
+
},
|
|
328
|
+
content: [],
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: 'function_call',
|
|
332
|
+
callId: 'tool_shell_1',
|
|
333
|
+
name: 'shell',
|
|
334
|
+
status: 'completed',
|
|
335
|
+
arguments: '{"command":"git status"}',
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
type: 'function_call_result',
|
|
339
|
+
callId: 'tool_shell_1',
|
|
340
|
+
name: 'shell',
|
|
341
|
+
status: 'completed',
|
|
342
|
+
output: { type: 'text', text: 'ok' },
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
await model.getResponse({
|
|
346
|
+
systemInstructions: 'system message',
|
|
347
|
+
input,
|
|
348
|
+
});
|
|
349
|
+
const body = requests[0];
|
|
350
|
+
t.truthy(body);
|
|
351
|
+
// Expect the reasoning blocks to be replayed as reasoning_details on the assistant tool_calls message.
|
|
352
|
+
const assistantToolCall = body.messages.find((m) => m.role === 'assistant' && Array.isArray(m.tool_calls));
|
|
353
|
+
t.truthy(assistantToolCall);
|
|
354
|
+
t.deepEqual(assistantToolCall.reasoning_details, [
|
|
355
|
+
{
|
|
356
|
+
type: 'reasoning.text',
|
|
357
|
+
text: 'step1',
|
|
358
|
+
format: 'google-gemini-v1',
|
|
359
|
+
index: 0,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: 'tool_shell_1',
|
|
363
|
+
type: 'reasoning.encrypted',
|
|
364
|
+
data: 'ENC',
|
|
365
|
+
format: 'google-gemini-v1',
|
|
366
|
+
index: 1,
|
|
367
|
+
},
|
|
368
|
+
]);
|
|
369
|
+
});
|
|
370
|
+
test.serial('stores reasoning_details/reasoning on message output items for future turns', async (t) => {
|
|
371
|
+
globalThis.fetch = async (_url, options) => {
|
|
372
|
+
JSON.parse(options.body);
|
|
373
|
+
return createJsonResponse({
|
|
374
|
+
id: 'resp-1',
|
|
375
|
+
choices: [
|
|
376
|
+
{
|
|
377
|
+
message: {
|
|
378
|
+
content: 'Hello back',
|
|
379
|
+
reasoning: 'Some thinking',
|
|
380
|
+
reasoning_details: [
|
|
381
|
+
{
|
|
382
|
+
type: 'reasoning.text',
|
|
383
|
+
text: 'step1',
|
|
384
|
+
format: null,
|
|
385
|
+
index: 0,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
usage: {
|
|
392
|
+
prompt_tokens: 1,
|
|
393
|
+
completion_tokens: 2,
|
|
394
|
+
total_tokens: 3,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
const model = new OpenRouterModel({
|
|
399
|
+
settingsService: mockSettingsService,
|
|
400
|
+
loggingService: logger,
|
|
401
|
+
modelId: 'mock-model',
|
|
402
|
+
});
|
|
403
|
+
const resp = await model.getResponse({
|
|
404
|
+
systemInstructions: 'system message',
|
|
405
|
+
input: 'hi',
|
|
406
|
+
});
|
|
407
|
+
const msg = resp.output.find((o) => o.type === 'message');
|
|
408
|
+
t.truthy(msg);
|
|
409
|
+
t.is(msg.content?.[0]?.text, 'Hello back');
|
|
410
|
+
t.is(msg.reasoning, 'Some thinking');
|
|
411
|
+
t.deepEqual(msg.reasoning_details, [
|
|
412
|
+
{ type: 'reasoning.text', text: 'step1', format: null, index: 0 },
|
|
413
|
+
]);
|
|
414
|
+
});
|
|
415
|
+
test.serial('passes through full reasoning request object (max_tokens/exclude) even without reasoningEffort', async (t) => {
|
|
416
|
+
const requests = [];
|
|
417
|
+
globalThis.fetch = async (_url, options) => {
|
|
418
|
+
const body = JSON.parse(options.body);
|
|
419
|
+
requests.push(body);
|
|
420
|
+
return createJsonResponse({
|
|
421
|
+
id: 'resp-reasoning-obj',
|
|
422
|
+
choices: [{ message: { content: 'ok' } }],
|
|
423
|
+
usage: {},
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
const model = new OpenRouterModel({
|
|
427
|
+
settingsService: mockSettingsService,
|
|
428
|
+
loggingService: logger,
|
|
429
|
+
modelId: 'mock-model',
|
|
430
|
+
});
|
|
431
|
+
await model.getResponse({
|
|
432
|
+
systemInstructions: 'system message',
|
|
433
|
+
input: 'hi',
|
|
434
|
+
modelSettings: {
|
|
435
|
+
reasoning: {
|
|
436
|
+
max_tokens: 2000,
|
|
437
|
+
exclude: false,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
t.truthy(requests[0]);
|
|
442
|
+
t.deepEqual(requests[0].reasoning, { max_tokens: 2000, exclude: false });
|
|
443
|
+
});
|
|
444
|
+
test.serial('streams reasoning details and stores assistant history', async (t) => {
|
|
445
|
+
const requests = [];
|
|
446
|
+
const encoder = new TextEncoder();
|
|
447
|
+
const stream = new ReadableStream({
|
|
448
|
+
start(controller) {
|
|
449
|
+
controller.enqueue(encoder.encode('data: {"id":"resp-stream","choices":[{"delta":{"content":"Hello","reasoning_details":[{"type":"reasoning.text","text":"step1","format":null,"index":0}]}}]}\n'));
|
|
450
|
+
controller.enqueue(encoder.encode('data: {"choices":[{"delta":{"content":"!"}}]}\n'));
|
|
451
|
+
controller.enqueue(encoder.encode('data: [DONE]\n'));
|
|
452
|
+
controller.close();
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
const responses = [
|
|
456
|
+
new Response(stream, {
|
|
457
|
+
status: 200,
|
|
458
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
459
|
+
}),
|
|
460
|
+
createJsonResponse({
|
|
461
|
+
id: 'resp-stream-2',
|
|
462
|
+
choices: [{ message: { content: 'Follow up' } }],
|
|
463
|
+
usage: {},
|
|
464
|
+
}),
|
|
465
|
+
];
|
|
466
|
+
let call = 0;
|
|
467
|
+
globalThis.fetch = async (_url, options) => {
|
|
468
|
+
const body = JSON.parse(options.body);
|
|
469
|
+
requests.push(body);
|
|
470
|
+
return responses[call++];
|
|
471
|
+
};
|
|
472
|
+
const model = new OpenRouterModel({
|
|
473
|
+
settingsService: mockSettingsService,
|
|
474
|
+
loggingService: logger,
|
|
475
|
+
modelId: 'mock-model',
|
|
476
|
+
});
|
|
477
|
+
const streamedEvents = [];
|
|
478
|
+
for await (const event of model.getStreamedResponse({
|
|
479
|
+
systemInstructions: 'system message',
|
|
480
|
+
input: 'Streaming input',
|
|
481
|
+
})) {
|
|
482
|
+
streamedEvents.push(event);
|
|
483
|
+
}
|
|
484
|
+
const doneEvent = streamedEvents.find(event => event.type === 'response_done');
|
|
485
|
+
t.truthy(doneEvent);
|
|
486
|
+
// First output item should be reasoning
|
|
487
|
+
const reasoningOutput = doneEvent.response.output[0];
|
|
488
|
+
t.is(reasoningOutput.type, 'reasoning');
|
|
489
|
+
t.is(reasoningOutput.content[0].text, 'step1');
|
|
490
|
+
t.is(reasoningOutput.providerData.type, 'reasoning.text');
|
|
491
|
+
// Second output item should be the message
|
|
492
|
+
t.is(doneEvent.response.output[1].content[0].text, 'Hello!');
|
|
493
|
+
});
|
|
494
|
+
test.serial('converts user message items in array inputs', async (t) => {
|
|
495
|
+
const requests = [];
|
|
496
|
+
globalThis.fetch = async (_url, options) => {
|
|
497
|
+
const body = JSON.parse(options.body);
|
|
498
|
+
requests.push(body);
|
|
499
|
+
return new Response(JSON.stringify({ choices: [{ message: { content: 'ok' } }], usage: {} }), {
|
|
500
|
+
status: 200,
|
|
501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
const model = new OpenRouterModel({
|
|
505
|
+
settingsService: mockSettingsService,
|
|
506
|
+
loggingService: logger,
|
|
507
|
+
modelId: 'mock-model',
|
|
508
|
+
});
|
|
509
|
+
await model.getResponse({
|
|
510
|
+
systemInstructions: 'system message',
|
|
511
|
+
input: [{ type: 'message', role: 'user', content: 'hi' }],
|
|
512
|
+
});
|
|
513
|
+
t.truthy(requests[0]);
|
|
514
|
+
t.deepEqual(requests[0].messages, [
|
|
515
|
+
{ role: 'system', content: 'system message' },
|
|
516
|
+
{ role: 'user', content: 'hi' },
|
|
517
|
+
]);
|
|
518
|
+
});
|
|
519
|
+
test.serial('handles summary reasoning details', async (t) => {
|
|
520
|
+
const requests = [];
|
|
521
|
+
globalThis.fetch = async (_url, options) => {
|
|
522
|
+
const body = JSON.parse(options.body);
|
|
523
|
+
requests.push(body);
|
|
524
|
+
return createJsonResponse({
|
|
525
|
+
id: 'resp-summary',
|
|
526
|
+
choices: [
|
|
527
|
+
{
|
|
528
|
+
message: {
|
|
529
|
+
content: '1 + 1 equals 2.\n\nThis is the fundamental rule of addition in basic arithmetic, where adding 1 to 1 results in 2. For example, if you have one apple and get one more, you have two apples.',
|
|
530
|
+
reasoning: 'First, the user asked "What is 1 + 1", which is a simple math question.\n',
|
|
531
|
+
reasoning_details: [
|
|
532
|
+
{
|
|
533
|
+
format: 'xai-responses-v1',
|
|
534
|
+
index: 0,
|
|
535
|
+
type: 'reasoning.summary',
|
|
536
|
+
summary: 'First, the user asked "What is 1 + 1", which is a simple math question.\n',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
usage: { prompt_tokens: 10, completion_tokens: 50, total_tokens: 60 },
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
const model = new OpenRouterModel({
|
|
546
|
+
settingsService: mockSettingsService,
|
|
547
|
+
loggingService: logger,
|
|
548
|
+
modelId: 'mock-model',
|
|
549
|
+
});
|
|
550
|
+
const response = await model.getResponse({
|
|
551
|
+
systemInstructions: 'system message',
|
|
552
|
+
input: 'What is 1 + 1',
|
|
553
|
+
});
|
|
554
|
+
// Should have two output items: reasoning and message
|
|
555
|
+
t.is(response.output.length, 2);
|
|
556
|
+
// First item should be summary reasoning
|
|
557
|
+
const reasoningOutput = response.output[0];
|
|
558
|
+
t.is(reasoningOutput.type, 'reasoning');
|
|
559
|
+
t.is(reasoningOutput.content.length, 1);
|
|
560
|
+
t.is(reasoningOutput.content[0].type, 'input_text');
|
|
561
|
+
t.is(reasoningOutput.content[0].text, 'First, the user asked "What is 1 + 1", which is a simple math question.\n');
|
|
562
|
+
t.is(reasoningOutput.providerData.type, 'reasoning.summary');
|
|
563
|
+
t.is(reasoningOutput.providerData.format, 'xai-responses-v1');
|
|
564
|
+
t.is(reasoningOutput.providerData.summary, 'First, the user asked "What is 1 + 1", which is a simple math question.\n');
|
|
565
|
+
// Second item should be the message
|
|
566
|
+
t.is(response.output[1].type, 'message');
|
|
567
|
+
t.truthy(response.output[1].content[0].text.includes('1 + 1 equals 2'));
|
|
568
|
+
});
|
|
569
|
+
test.serial('handles encrypted reasoning details', async (t) => {
|
|
570
|
+
const requests = [];
|
|
571
|
+
globalThis.fetch = async (_url, options) => {
|
|
572
|
+
const body = JSON.parse(options.body);
|
|
573
|
+
requests.push(body);
|
|
574
|
+
return createJsonResponse({
|
|
575
|
+
id: 'resp-encrypted',
|
|
576
|
+
choices: [
|
|
577
|
+
{
|
|
578
|
+
message: {
|
|
579
|
+
content: 'Response with encrypted reasoning',
|
|
580
|
+
reasoning_details: [
|
|
581
|
+
{
|
|
582
|
+
id: 'rs_05c9be144fb3fa3b016936cb941e48819183e63b141a094cac',
|
|
583
|
+
format: 'openai-responses-v1',
|
|
584
|
+
index: 0,
|
|
585
|
+
type: 'reasoning.encrypted',
|
|
586
|
+
data: 'gAAAAABpNsuVfXBOvLwub1...',
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
const model = new OpenRouterModel({
|
|
596
|
+
settingsService: mockSettingsService,
|
|
597
|
+
loggingService: logger,
|
|
598
|
+
modelId: 'mock-model',
|
|
599
|
+
});
|
|
600
|
+
const response = await model.getResponse({
|
|
601
|
+
systemInstructions: 'system message',
|
|
602
|
+
input: 'Test encrypted reasoning',
|
|
603
|
+
});
|
|
604
|
+
// Should have two output items: reasoning and message
|
|
605
|
+
t.is(response.output.length, 2);
|
|
606
|
+
// First item should be encrypted reasoning
|
|
607
|
+
const reasoningOutput = response.output[0];
|
|
608
|
+
t.is(reasoningOutput.type, 'reasoning');
|
|
609
|
+
t.deepEqual(reasoningOutput.content, []); // No displayable content for encrypted
|
|
610
|
+
t.is(reasoningOutput.providerData.type, 'reasoning.encrypted');
|
|
611
|
+
t.is(reasoningOutput.providerData.id, 'rs_05c9be144fb3fa3b016936cb941e48819183e63b141a094cac');
|
|
612
|
+
t.is(reasoningOutput.providerData.format, 'openai-responses-v1');
|
|
613
|
+
t.is(reasoningOutput.providerData.data, 'gAAAAABpNsuVfXBOvLwub1...');
|
|
614
|
+
// Second item should be the message
|
|
615
|
+
t.is(response.output[1].type, 'message');
|
|
616
|
+
t.is(response.output[1].content[0].text, 'Response with encrypted reasoning');
|
|
617
|
+
});
|
|
618
|
+
test.serial('adds fallback assistant message when response has empty content', async (t) => {
|
|
619
|
+
globalThis.fetch = async (_url, options) => {
|
|
620
|
+
const body = JSON.parse(options.body);
|
|
621
|
+
t.truthy(body);
|
|
622
|
+
return createJsonResponse({
|
|
623
|
+
id: 'resp-empty-content',
|
|
624
|
+
choices: [
|
|
625
|
+
{
|
|
626
|
+
message: {
|
|
627
|
+
content: '',
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
usage: {
|
|
632
|
+
prompt_tokens: 1,
|
|
633
|
+
completion_tokens: 0,
|
|
634
|
+
total_tokens: 1,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
const model = new OpenRouterModel({
|
|
639
|
+
settingsService: mockSettingsService,
|
|
640
|
+
loggingService: logger,
|
|
641
|
+
modelId: 'mock-model',
|
|
642
|
+
});
|
|
643
|
+
const response = await model.getResponse({
|
|
644
|
+
systemInstructions: 'system message',
|
|
645
|
+
input: 'Hello?',
|
|
646
|
+
});
|
|
647
|
+
t.is(response.output.length, 1);
|
|
648
|
+
t.is(response.output[0].type, 'message');
|
|
649
|
+
t.is(response.output[0].content[0].text, 'No response from model.');
|
|
650
|
+
});
|
|
651
|
+
test.serial('correctly converts function_call_output to tool message', async (t) => {
|
|
652
|
+
const requests = [];
|
|
653
|
+
globalThis.fetch = async (_url, options) => {
|
|
654
|
+
const body = JSON.parse(options.body);
|
|
655
|
+
requests.push(body);
|
|
656
|
+
return createJsonResponse({
|
|
657
|
+
id: 'resp-1',
|
|
658
|
+
choices: [{ message: { content: 'ok' } }],
|
|
659
|
+
usage: {
|
|
660
|
+
prompt_tokens: 1,
|
|
661
|
+
completion_tokens: 1,
|
|
662
|
+
total_tokens: 2,
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
const model = new OpenRouterModel({
|
|
667
|
+
settingsService: mockSettingsService,
|
|
668
|
+
loggingService: logger,
|
|
669
|
+
modelId: 'mock-model',
|
|
670
|
+
});
|
|
671
|
+
// Simulate a conversation turn with function_call_output
|
|
672
|
+
const inputItems = [
|
|
673
|
+
{ type: 'input_text', text: 'User message' },
|
|
674
|
+
{
|
|
675
|
+
type: 'function_call',
|
|
676
|
+
id: 'call-abc',
|
|
677
|
+
name: 'search_replace',
|
|
678
|
+
arguments: '{"path":"test.txt","search_content":"old","replace_content":"new"}',
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
rawItem: {
|
|
682
|
+
type: 'function_call_output',
|
|
683
|
+
callId: 'call-abc',
|
|
684
|
+
id: 'call-abc',
|
|
685
|
+
name: 'search_replace',
|
|
686
|
+
output: '{"output":[{"success":true}]}',
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
];
|
|
690
|
+
await model.getResponse({
|
|
691
|
+
systemInstructions: 'system message',
|
|
692
|
+
input: inputItems,
|
|
693
|
+
});
|
|
694
|
+
t.truthy(requests[0]);
|
|
695
|
+
const messages = requests[0].messages;
|
|
696
|
+
// Should have: system, user, assistant (with tool_calls), tool (with result)
|
|
697
|
+
t.is(messages.length, 4);
|
|
698
|
+
t.is(messages[0].role, 'system');
|
|
699
|
+
t.is(messages[1].role, 'user');
|
|
700
|
+
t.is(messages[1].content, 'User message');
|
|
701
|
+
// function_call should be converted to assistant message with tool_calls
|
|
702
|
+
t.is(messages[2].role, 'assistant');
|
|
703
|
+
t.is(messages[2].content, null);
|
|
704
|
+
t.truthy(messages[2].tool_calls);
|
|
705
|
+
t.is(messages[2].tool_calls[0].id, 'call-abc');
|
|
706
|
+
t.is(messages[2].tool_calls[0].function.name, 'search_replace');
|
|
707
|
+
// function_call_output should be converted to tool message, NOT another assistant message
|
|
708
|
+
t.is(messages[3].role, 'tool');
|
|
709
|
+
t.is(messages[3].tool_call_id, 'call-abc');
|
|
710
|
+
t.truthy(messages[3].content);
|
|
711
|
+
});
|
|
712
|
+
test.serial('handles mixed reasoning types (summary + encrypted)', async (t) => {
|
|
713
|
+
const requests = [];
|
|
714
|
+
globalThis.fetch = async (_url, options) => {
|
|
715
|
+
const body = JSON.parse(options.body);
|
|
716
|
+
requests.push(body);
|
|
717
|
+
return createJsonResponse({
|
|
718
|
+
id: 'resp-mixed',
|
|
719
|
+
choices: [
|
|
720
|
+
{
|
|
721
|
+
message: {
|
|
722
|
+
content: '1 + 1 equals 2.\n\nThis is the fundamental rule of addition.',
|
|
723
|
+
reasoning: 'First, the user asked "What is 1 + 1", which is a simple math question.\n',
|
|
724
|
+
reasoning_details: [
|
|
725
|
+
{
|
|
726
|
+
format: 'xai-responses-v1',
|
|
727
|
+
index: 0,
|
|
728
|
+
type: 'reasoning.summary',
|
|
729
|
+
summary: 'First, the user asked "What is 1 + 1", which is a simple math question.\n',
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: 'rs_b908e80d-7948-923b-a82b-0a61eba37a92',
|
|
733
|
+
format: 'xai-responses-v1',
|
|
734
|
+
index: 1,
|
|
735
|
+
type: 'reasoning.encrypted',
|
|
736
|
+
data: 'zDzBlL2u7sDbq1l6rOpNL',
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
usage: { prompt_tokens: 10, completion_tokens: 50, total_tokens: 60 },
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
const model = new OpenRouterModel({
|
|
746
|
+
settingsService: mockSettingsService,
|
|
747
|
+
loggingService: logger,
|
|
748
|
+
modelId: 'mock-model',
|
|
749
|
+
});
|
|
750
|
+
const response = await model.getResponse({
|
|
751
|
+
systemInstructions: 'system message',
|
|
752
|
+
input: 'What is 1 + 1',
|
|
753
|
+
});
|
|
754
|
+
// Should have three output items: summary reasoning, encrypted reasoning, and message
|
|
755
|
+
t.is(response.output.length, 3);
|
|
756
|
+
// First item should be summary reasoning
|
|
757
|
+
const summaryOutput = response.output[0];
|
|
758
|
+
t.is(summaryOutput.type, 'reasoning');
|
|
759
|
+
t.is(summaryOutput.content[0].text, 'First, the user asked "What is 1 + 1", which is a simple math question.\n');
|
|
760
|
+
t.is(summaryOutput.providerData.type, 'reasoning.summary');
|
|
761
|
+
// Second item should be encrypted reasoning
|
|
762
|
+
const encryptedOutput = response.output[1];
|
|
763
|
+
t.is(encryptedOutput.type, 'reasoning');
|
|
764
|
+
t.deepEqual(encryptedOutput.content, []); // No displayable content
|
|
765
|
+
t.is(encryptedOutput.providerData.type, 'reasoning.encrypted');
|
|
766
|
+
t.is(encryptedOutput.providerData.data, 'zDzBlL2u7sDbq1l6rOpNL');
|
|
767
|
+
// Third item should be the message
|
|
768
|
+
t.is(response.output[2].type, 'message');
|
|
769
|
+
t.truthy(response.output[2].content[0].text.includes('1 + 1 equals 2'));
|
|
770
|
+
});
|
|
771
|
+
test.serial('applies cache_control to last tool message for Anthropic models', async (t) => {
|
|
772
|
+
const requests = [];
|
|
773
|
+
globalThis.fetch = async (_url, options) => {
|
|
774
|
+
const body = JSON.parse(options.body);
|
|
775
|
+
requests.push(body);
|
|
776
|
+
return createJsonResponse({
|
|
777
|
+
id: 'resp-1',
|
|
778
|
+
choices: [{ message: { content: 'ok' } }],
|
|
779
|
+
usage: {},
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
const model = new OpenRouterModel({
|
|
783
|
+
settingsService: mockSettingsService,
|
|
784
|
+
loggingService: logger,
|
|
785
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
786
|
+
});
|
|
787
|
+
const inputItems = [
|
|
788
|
+
{ type: 'input_text', text: 'User message' },
|
|
789
|
+
{
|
|
790
|
+
type: 'function_call',
|
|
791
|
+
id: 'call-1',
|
|
792
|
+
name: 'tool1',
|
|
793
|
+
arguments: '{}',
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
type: 'function_call_result',
|
|
797
|
+
callId: 'call-1',
|
|
798
|
+
output: 'result1',
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
type: 'function_call',
|
|
802
|
+
id: 'call-2',
|
|
803
|
+
name: 'tool2',
|
|
804
|
+
arguments: '{}',
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
type: 'function_call_result',
|
|
808
|
+
callId: 'call-2',
|
|
809
|
+
output: 'result2',
|
|
810
|
+
},
|
|
811
|
+
];
|
|
812
|
+
await model.getResponse({
|
|
813
|
+
systemInstructions: 'system',
|
|
814
|
+
input: inputItems,
|
|
815
|
+
});
|
|
816
|
+
t.truthy(requests[0]);
|
|
817
|
+
const messages = requests[0].messages;
|
|
818
|
+
// Verify last tool message has cache_control
|
|
819
|
+
const lastToolMsg = messages[messages.length - 1];
|
|
820
|
+
t.is(lastToolMsg.role, 'tool');
|
|
821
|
+
t.is(lastToolMsg.tool_call_id, 'call-2');
|
|
822
|
+
t.true(Array.isArray(lastToolMsg.content));
|
|
823
|
+
t.is(lastToolMsg.content[0].text, 'result2');
|
|
824
|
+
t.deepEqual(lastToolMsg.content[0].cache_control, { type: 'ephemeral' });
|
|
825
|
+
// Verify previous tool message does NOT have cache_control
|
|
826
|
+
const firstToolMsg = messages.find((m) => m.tool_call_id === 'call-1');
|
|
827
|
+
t.truthy(firstToolMsg);
|
|
828
|
+
t.is(typeof firstToolMsg.content, 'string');
|
|
829
|
+
t.is(firstToolMsg.content, 'result1');
|
|
830
|
+
});
|
|
831
|
+
test.serial('preserves assistant message content when tool calls are present', async (t) => {
|
|
832
|
+
const requests = [];
|
|
833
|
+
globalThis.fetch = async (_url, options) => {
|
|
834
|
+
const body = JSON.parse(options.body);
|
|
835
|
+
requests.push(body);
|
|
836
|
+
return createJsonResponse({
|
|
837
|
+
id: 'resp-with-tools',
|
|
838
|
+
choices: [{ message: { content: 'Response after tools' } }],
|
|
839
|
+
usage: {
|
|
840
|
+
prompt_tokens: 5,
|
|
841
|
+
completion_tokens: 3,
|
|
842
|
+
total_tokens: 8,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
};
|
|
846
|
+
const model = new OpenRouterModel({
|
|
847
|
+
settingsService: mockSettingsService,
|
|
848
|
+
loggingService: logger,
|
|
849
|
+
modelId: 'mock-model',
|
|
850
|
+
});
|
|
851
|
+
// First turn - get initial response
|
|
852
|
+
await model.getResponse({
|
|
853
|
+
systemInstructions: 'system message',
|
|
854
|
+
input: 'First turn',
|
|
855
|
+
});
|
|
856
|
+
// Second turn - assistant message with both content and tool calls
|
|
857
|
+
await model.getResponse({
|
|
858
|
+
systemInstructions: 'system message',
|
|
859
|
+
input: [
|
|
860
|
+
{ type: 'input_text', text: 'User message' },
|
|
861
|
+
{
|
|
862
|
+
type: 'message',
|
|
863
|
+
role: 'assistant',
|
|
864
|
+
content: [{ type: 'output_text', text: 'I will use a tool' }],
|
|
865
|
+
tool_calls: [
|
|
866
|
+
{
|
|
867
|
+
id: 'call-456',
|
|
868
|
+
type: 'function',
|
|
869
|
+
function: {
|
|
870
|
+
name: 'search',
|
|
871
|
+
arguments: '{"query":"test"}',
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
type: 'function_call_result',
|
|
878
|
+
callId: 'call-456',
|
|
879
|
+
output: 'search results',
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
});
|
|
883
|
+
const secondRequest = requests[1];
|
|
884
|
+
t.truthy(secondRequest);
|
|
885
|
+
// Find the assistant message with tool calls
|
|
886
|
+
const assistantMsg = secondRequest.messages.find((m) => m.role === 'assistant' && m.tool_calls);
|
|
887
|
+
t.truthy(assistantMsg);
|
|
888
|
+
// Verify both content and tool_calls are preserved
|
|
889
|
+
t.is(assistantMsg.content, 'I will use a tool');
|
|
890
|
+
t.truthy(assistantMsg.tool_calls);
|
|
891
|
+
t.is(assistantMsg.tool_calls.length, 1);
|
|
892
|
+
t.is(assistantMsg.tool_calls[0].id, 'call-456');
|
|
893
|
+
t.is(assistantMsg.tool_calls[0].function.name, 'search');
|
|
894
|
+
});
|
|
895
|
+
// ========== Error Recovery Tests: OpenRouterError and Retry Classification ==========
|
|
896
|
+
test('OpenRouterError includes status and headers', t => {
|
|
897
|
+
const error = new OpenRouterError('Test error', 429, { 'retry-after': '5', 'x-custom': 'value' }, 'response body');
|
|
898
|
+
t.is(error.message, 'Test error');
|
|
899
|
+
t.is(error.status, 429);
|
|
900
|
+
t.deepEqual(error.headers, { 'retry-after': '5', 'x-custom': 'value' });
|
|
901
|
+
t.is(error.responseBody, 'response body');
|
|
902
|
+
t.is(error.name, 'OpenRouterError');
|
|
903
|
+
});
|
|
904
|
+
test('OpenRouterError is throwable', t => {
|
|
905
|
+
const fn = () => {
|
|
906
|
+
throw new OpenRouterError('Error message', 500, {});
|
|
907
|
+
};
|
|
908
|
+
const error = t.throws(fn);
|
|
909
|
+
t.true(error instanceof OpenRouterError);
|
|
910
|
+
if (error instanceof OpenRouterError) {
|
|
911
|
+
t.is(error.status, 500);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
test('OpenRouterError can be created with minimal arguments', t => {
|
|
915
|
+
const error = new OpenRouterError('Minimal error', 400, {});
|
|
916
|
+
t.is(error.message, 'Minimal error');
|
|
917
|
+
t.is(error.status, 400);
|
|
918
|
+
t.deepEqual(error.headers, {});
|
|
919
|
+
t.is(error.responseBody, undefined);
|
|
920
|
+
});
|
|
921
|
+
test('OpenRouterError preserves response body', t => {
|
|
922
|
+
const responseBody = '{"error": "Rate limit exceeded"}';
|
|
923
|
+
const error = new OpenRouterError('Rate limited', 429, {}, responseBody);
|
|
924
|
+
t.is(error.responseBody, responseBody);
|
|
925
|
+
});
|
|
926
|
+
test('OpenRouterError with 429 status for retry logic', t => {
|
|
927
|
+
const error = new OpenRouterError('Too many requests', 429, {
|
|
928
|
+
'retry-after': '60',
|
|
929
|
+
});
|
|
930
|
+
t.is(error.status, 429);
|
|
931
|
+
t.is(error.headers['retry-after'], '60');
|
|
932
|
+
});
|
|
933
|
+
test('OpenRouterError with 5xx status codes for retry logic', t => {
|
|
934
|
+
const statuses = [500, 502, 503, 504];
|
|
935
|
+
for (const status of statuses) {
|
|
936
|
+
const error = new OpenRouterError(`Server error ${status}`, status, {});
|
|
937
|
+
t.is(error.status, status);
|
|
938
|
+
t.true(error.status >= 500);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
test('OpenRouterError with 4xx status codes (non-retryable)', t => {
|
|
942
|
+
const statuses = [400, 401, 403, 404, 422];
|
|
943
|
+
for (const status of statuses) {
|
|
944
|
+
const error = new OpenRouterError(`Client error ${status}`, status, {});
|
|
945
|
+
t.is(error.status, status);
|
|
946
|
+
t.true(error.status >= 400 && error.status < 500);
|
|
947
|
+
t.not(error.status, 429); // 429 is retryable
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
test('OpenRouterError with headers for Retry-After extraction', t => {
|
|
951
|
+
const headers = {
|
|
952
|
+
'retry-after': '120',
|
|
953
|
+
'x-ratelimit-reset': '1234567890',
|
|
954
|
+
'content-type': 'application/json',
|
|
955
|
+
};
|
|
956
|
+
const error = new OpenRouterError('Rate limit', 429, headers);
|
|
957
|
+
t.is(error.headers['retry-after'], '120');
|
|
958
|
+
t.is(error.headers['x-ratelimit-reset'], '1234567890');
|
|
959
|
+
t.is(error.headers['content-type'], 'application/json');
|
|
960
|
+
});
|
|
961
|
+
test('OpenRouterError error instanceof checks', t => {
|
|
962
|
+
const error = new OpenRouterError('Test', 500, {});
|
|
963
|
+
t.true(error instanceof Error);
|
|
964
|
+
t.true(error instanceof OpenRouterError);
|
|
965
|
+
});
|
|
966
|
+
test('OpenRouterError with empty headers', t => {
|
|
967
|
+
const error = new OpenRouterError('No headers', 503, {});
|
|
968
|
+
t.deepEqual(error.headers, {});
|
|
969
|
+
t.is(Object.keys(error.headers).length, 0);
|
|
970
|
+
});
|
|
971
|
+
// Anthropic prompt caching tests
|
|
972
|
+
test.serial('applies cache_control to system message for Anthropic models', async (t) => {
|
|
973
|
+
const requests = [];
|
|
974
|
+
globalThis.fetch = async (_url, options) => {
|
|
975
|
+
const body = JSON.parse(options.body);
|
|
976
|
+
requests.push(body);
|
|
977
|
+
return createJsonResponse({
|
|
978
|
+
id: 'resp-1',
|
|
979
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
980
|
+
usage: {
|
|
981
|
+
prompt_tokens: 10,
|
|
982
|
+
completion_tokens: 5,
|
|
983
|
+
total_tokens: 15,
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
};
|
|
987
|
+
const model = new OpenRouterModel({
|
|
988
|
+
settingsService: mockSettingsService,
|
|
989
|
+
loggingService: logger,
|
|
990
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
991
|
+
});
|
|
992
|
+
await model.getResponse({
|
|
993
|
+
systemInstructions: 'You are a helpful assistant.',
|
|
994
|
+
input: 'Hello',
|
|
995
|
+
});
|
|
996
|
+
t.truthy(requests[0]);
|
|
997
|
+
t.deepEqual(requests[0].messages[0], {
|
|
998
|
+
role: 'system',
|
|
999
|
+
content: [
|
|
1000
|
+
{
|
|
1001
|
+
type: 'text',
|
|
1002
|
+
text: 'You are a helpful assistant.',
|
|
1003
|
+
cache_control: { type: 'ephemeral' },
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
test.serial('applies cache_control for Claude model variants', async (t) => {
|
|
1009
|
+
const requests = [];
|
|
1010
|
+
globalThis.fetch = async (_url, options) => {
|
|
1011
|
+
const body = JSON.parse(options.body);
|
|
1012
|
+
requests.push(body);
|
|
1013
|
+
return createJsonResponse({
|
|
1014
|
+
id: 'resp-1',
|
|
1015
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
1016
|
+
usage: {},
|
|
1017
|
+
});
|
|
1018
|
+
};
|
|
1019
|
+
// Test with different Claude model IDs
|
|
1020
|
+
const claudeModels = [
|
|
1021
|
+
'anthropic/claude-3-opus',
|
|
1022
|
+
'anthropic/claude-3.5-sonnet',
|
|
1023
|
+
'anthropic/claude-3-haiku',
|
|
1024
|
+
'openrouter/claude-instant',
|
|
1025
|
+
];
|
|
1026
|
+
for (const modelId of claudeModels) {
|
|
1027
|
+
requests.length = 0;
|
|
1028
|
+
const model = new OpenRouterModel({
|
|
1029
|
+
settingsService: mockSettingsService,
|
|
1030
|
+
loggingService: logger,
|
|
1031
|
+
modelId,
|
|
1032
|
+
});
|
|
1033
|
+
await model.getResponse({
|
|
1034
|
+
systemInstructions: 'System prompt',
|
|
1035
|
+
input: 'Test',
|
|
1036
|
+
});
|
|
1037
|
+
t.truthy(requests[0], `Failed for model: ${modelId}`);
|
|
1038
|
+
t.is(requests[0].messages[0].role, 'system');
|
|
1039
|
+
t.true(Array.isArray(requests[0].messages[0].content), `Content should be array for ${modelId}`);
|
|
1040
|
+
t.deepEqual(requests[0].messages[0].content[0].cache_control, { type: 'ephemeral' }, `cache_control should be set for ${modelId}`);
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
test.serial('does not apply cache_control for non-Anthropic models', async (t) => {
|
|
1044
|
+
const requests = [];
|
|
1045
|
+
globalThis.fetch = async (_url, options) => {
|
|
1046
|
+
const body = JSON.parse(options.body);
|
|
1047
|
+
requests.push(body);
|
|
1048
|
+
return createJsonResponse({
|
|
1049
|
+
id: 'resp-1',
|
|
1050
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
1051
|
+
usage: {},
|
|
1052
|
+
});
|
|
1053
|
+
};
|
|
1054
|
+
// Test with non-Anthropic models
|
|
1055
|
+
const nonAnthropicModels = [
|
|
1056
|
+
'openai/gpt-4o',
|
|
1057
|
+
'google/gemini-pro',
|
|
1058
|
+
'meta-llama/llama-3-70b',
|
|
1059
|
+
'mistralai/mistral-large',
|
|
1060
|
+
'openrouter/auto',
|
|
1061
|
+
];
|
|
1062
|
+
for (const modelId of nonAnthropicModels) {
|
|
1063
|
+
requests.length = 0;
|
|
1064
|
+
const model = new OpenRouterModel({
|
|
1065
|
+
settingsService: mockSettingsService,
|
|
1066
|
+
loggingService: logger,
|
|
1067
|
+
modelId,
|
|
1068
|
+
});
|
|
1069
|
+
await model.getResponse({
|
|
1070
|
+
systemInstructions: 'System prompt',
|
|
1071
|
+
input: 'Test',
|
|
1072
|
+
});
|
|
1073
|
+
t.truthy(requests[0], `Failed for model: ${modelId}`);
|
|
1074
|
+
t.deepEqual(requests[0].messages[0], { role: 'system', content: 'System prompt' }, `System message should be plain string for ${modelId}`);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
test.serial('applies cache_control in streamed response for Anthropic models', async (t) => {
|
|
1078
|
+
const requests = [];
|
|
1079
|
+
// Create a simple SSE stream response
|
|
1080
|
+
const sseData = [
|
|
1081
|
+
'data: {"id":"resp-1","choices":[{"delta":{"content":"Hi"}}]}\n\n',
|
|
1082
|
+
'data: {"id":"resp-1","choices":[{"delta":{"content":"!"}}],"usage":{"prompt_tokens":10,"completion_tokens":2}}\n\n',
|
|
1083
|
+
'data: [DONE]\n\n',
|
|
1084
|
+
].join('');
|
|
1085
|
+
globalThis.fetch = async (_url, options) => {
|
|
1086
|
+
const body = JSON.parse(options.body);
|
|
1087
|
+
requests.push(body);
|
|
1088
|
+
const stream = new ReadableStream({
|
|
1089
|
+
start(controller) {
|
|
1090
|
+
controller.enqueue(new TextEncoder().encode(sseData));
|
|
1091
|
+
controller.close();
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
return new Response(stream, {
|
|
1095
|
+
status: 200,
|
|
1096
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
1097
|
+
});
|
|
1098
|
+
};
|
|
1099
|
+
const model = new OpenRouterModel({
|
|
1100
|
+
settingsService: mockSettingsService,
|
|
1101
|
+
loggingService: logger,
|
|
1102
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
1103
|
+
});
|
|
1104
|
+
// Consume the stream
|
|
1105
|
+
for await (const _event of model.getStreamedResponse({
|
|
1106
|
+
systemInstructions: 'You are helpful.',
|
|
1107
|
+
input: 'Hi',
|
|
1108
|
+
})) {
|
|
1109
|
+
// Just consume the events
|
|
1110
|
+
}
|
|
1111
|
+
t.truthy(requests[0]);
|
|
1112
|
+
t.deepEqual(requests[0].messages[0], {
|
|
1113
|
+
role: 'system',
|
|
1114
|
+
content: [
|
|
1115
|
+
{
|
|
1116
|
+
type: 'text',
|
|
1117
|
+
text: 'You are helpful.',
|
|
1118
|
+
cache_control: { type: 'ephemeral' },
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
// Tests for last user message caching (using 2 of 4 cache points efficiently)
|
|
1124
|
+
test.serial('applies cache_control to last user message for Anthropic models (string content)', async (t) => {
|
|
1125
|
+
const requests = [];
|
|
1126
|
+
globalThis.fetch = async (_url, options) => {
|
|
1127
|
+
const body = JSON.parse(options.body);
|
|
1128
|
+
requests.push(body);
|
|
1129
|
+
return createJsonResponse({
|
|
1130
|
+
id: 'resp-1',
|
|
1131
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
1132
|
+
usage: {
|
|
1133
|
+
prompt_tokens: 10,
|
|
1134
|
+
completion_tokens: 5,
|
|
1135
|
+
total_tokens: 15,
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
};
|
|
1139
|
+
const model = new OpenRouterModel({
|
|
1140
|
+
settingsService: mockSettingsService,
|
|
1141
|
+
loggingService: logger,
|
|
1142
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
1143
|
+
});
|
|
1144
|
+
await model.getResponse({
|
|
1145
|
+
systemInstructions: 'You are a helpful assistant.',
|
|
1146
|
+
input: 'Hello world',
|
|
1147
|
+
});
|
|
1148
|
+
t.truthy(requests[0]);
|
|
1149
|
+
// Verify system message has cache_control
|
|
1150
|
+
t.deepEqual(requests[0].messages[0], {
|
|
1151
|
+
role: 'system',
|
|
1152
|
+
content: [
|
|
1153
|
+
{
|
|
1154
|
+
type: 'text',
|
|
1155
|
+
text: 'You are a helpful assistant.',
|
|
1156
|
+
cache_control: { type: 'ephemeral' },
|
|
1157
|
+
},
|
|
1158
|
+
],
|
|
1159
|
+
});
|
|
1160
|
+
// Verify last user message has cache_control (converted from string to array)
|
|
1161
|
+
t.deepEqual(requests[0].messages[1], {
|
|
1162
|
+
role: 'user',
|
|
1163
|
+
content: [
|
|
1164
|
+
{
|
|
1165
|
+
type: 'text',
|
|
1166
|
+
text: 'Hello world',
|
|
1167
|
+
cache_control: { type: 'ephemeral' },
|
|
1168
|
+
},
|
|
1169
|
+
],
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
test.serial('applies cache_control to last user message in multi-turn conversation', async (t) => {
|
|
1173
|
+
const requests = [];
|
|
1174
|
+
globalThis.fetch = async (_url, options) => {
|
|
1175
|
+
const body = JSON.parse(options.body);
|
|
1176
|
+
requests.push(body);
|
|
1177
|
+
return createJsonResponse({
|
|
1178
|
+
id: 'resp-1',
|
|
1179
|
+
choices: [{ message: { content: 'Response' } }],
|
|
1180
|
+
usage: {},
|
|
1181
|
+
});
|
|
1182
|
+
};
|
|
1183
|
+
const model = new OpenRouterModel({
|
|
1184
|
+
settingsService: mockSettingsService,
|
|
1185
|
+
loggingService: logger,
|
|
1186
|
+
modelId: 'anthropic/claude-3-opus',
|
|
1187
|
+
});
|
|
1188
|
+
// Simulate a multi-turn conversation
|
|
1189
|
+
const conversationItems = [
|
|
1190
|
+
{ type: 'message', role: 'user', content: 'First question' },
|
|
1191
|
+
{
|
|
1192
|
+
type: 'message',
|
|
1193
|
+
role: 'assistant',
|
|
1194
|
+
content: [{ type: 'output_text', text: 'First answer' }],
|
|
1195
|
+
status: 'completed',
|
|
1196
|
+
},
|
|
1197
|
+
{ type: 'message', role: 'user', content: 'Second question' },
|
|
1198
|
+
{
|
|
1199
|
+
type: 'message',
|
|
1200
|
+
role: 'assistant',
|
|
1201
|
+
content: [{ type: 'output_text', text: 'Second answer' }],
|
|
1202
|
+
status: 'completed',
|
|
1203
|
+
},
|
|
1204
|
+
{ type: 'input_text', text: 'Third question' }, // Latest user input
|
|
1205
|
+
];
|
|
1206
|
+
await model.getResponse({
|
|
1207
|
+
systemInstructions: 'System prompt',
|
|
1208
|
+
input: conversationItems,
|
|
1209
|
+
});
|
|
1210
|
+
t.truthy(requests[0]);
|
|
1211
|
+
const messages = requests[0].messages;
|
|
1212
|
+
// Find all user messages
|
|
1213
|
+
const userMessages = messages.filter((m) => m.role === 'user');
|
|
1214
|
+
t.is(userMessages.length, 3, 'Should have 3 user messages');
|
|
1215
|
+
// First two user messages should NOT have cache_control (plain string)
|
|
1216
|
+
t.is(userMessages[0].content, 'First question', 'First user message should be plain string');
|
|
1217
|
+
t.is(userMessages[1].content, 'Second question', 'Second user message should be plain string');
|
|
1218
|
+
// Only the LAST user message should have cache_control (array format)
|
|
1219
|
+
t.deepEqual(userMessages[2], {
|
|
1220
|
+
role: 'user',
|
|
1221
|
+
content: [
|
|
1222
|
+
{
|
|
1223
|
+
type: 'text',
|
|
1224
|
+
text: 'Third question',
|
|
1225
|
+
cache_control: { type: 'ephemeral' },
|
|
1226
|
+
},
|
|
1227
|
+
],
|
|
1228
|
+
}, 'Last user message should have cache_control');
|
|
1229
|
+
});
|
|
1230
|
+
test.serial('does not apply cache_control to user messages for non-Anthropic models', async (t) => {
|
|
1231
|
+
const requests = [];
|
|
1232
|
+
globalThis.fetch = async (_url, options) => {
|
|
1233
|
+
const body = JSON.parse(options.body);
|
|
1234
|
+
requests.push(body);
|
|
1235
|
+
return createJsonResponse({
|
|
1236
|
+
id: 'resp-1',
|
|
1237
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
1238
|
+
usage: {},
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
const model = new OpenRouterModel({
|
|
1242
|
+
settingsService: mockSettingsService,
|
|
1243
|
+
loggingService: logger,
|
|
1244
|
+
modelId: 'openai/gpt-4o',
|
|
1245
|
+
});
|
|
1246
|
+
await model.getResponse({
|
|
1247
|
+
systemInstructions: 'System prompt',
|
|
1248
|
+
input: 'Hello',
|
|
1249
|
+
});
|
|
1250
|
+
t.truthy(requests[0]);
|
|
1251
|
+
// System message should be plain string for non-Anthropic
|
|
1252
|
+
t.deepEqual(requests[0].messages[0], {
|
|
1253
|
+
role: 'system',
|
|
1254
|
+
content: 'System prompt',
|
|
1255
|
+
});
|
|
1256
|
+
// User message should also be plain string
|
|
1257
|
+
t.deepEqual(requests[0].messages[1], { role: 'user', content: 'Hello' });
|
|
1258
|
+
});
|
|
1259
|
+
test.serial('handles conversation with no user messages gracefully for Anthropic models', async (t) => {
|
|
1260
|
+
const requests = [];
|
|
1261
|
+
globalThis.fetch = async (_url, options) => {
|
|
1262
|
+
const body = JSON.parse(options.body);
|
|
1263
|
+
requests.push(body);
|
|
1264
|
+
return createJsonResponse({
|
|
1265
|
+
id: 'resp-1',
|
|
1266
|
+
choices: [{ message: { content: 'Hello' } }],
|
|
1267
|
+
usage: {},
|
|
1268
|
+
});
|
|
1269
|
+
};
|
|
1270
|
+
const model = new OpenRouterModel({
|
|
1271
|
+
settingsService: mockSettingsService,
|
|
1272
|
+
loggingService: logger,
|
|
1273
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
1274
|
+
});
|
|
1275
|
+
// Pass empty input array (edge case)
|
|
1276
|
+
await model.getResponse({
|
|
1277
|
+
systemInstructions: 'System prompt',
|
|
1278
|
+
input: [],
|
|
1279
|
+
});
|
|
1280
|
+
t.truthy(requests[0]);
|
|
1281
|
+
// Should still have system message with cache_control
|
|
1282
|
+
t.deepEqual(requests[0].messages[0], {
|
|
1283
|
+
role: 'system',
|
|
1284
|
+
content: [
|
|
1285
|
+
{
|
|
1286
|
+
type: 'text',
|
|
1287
|
+
text: 'System prompt',
|
|
1288
|
+
cache_control: { type: 'ephemeral' },
|
|
1289
|
+
},
|
|
1290
|
+
],
|
|
1291
|
+
});
|
|
1292
|
+
// Should only have system message, no user messages
|
|
1293
|
+
t.is(requests[0].messages.length, 1);
|
|
1294
|
+
});
|
|
1295
|
+
test.serial('both system and last user message have cache_control for Anthropic', async (t) => {
|
|
1296
|
+
const requests = [];
|
|
1297
|
+
globalThis.fetch = async (_url, options) => {
|
|
1298
|
+
const body = JSON.parse(options.body);
|
|
1299
|
+
requests.push(body);
|
|
1300
|
+
return createJsonResponse({
|
|
1301
|
+
id: 'resp-1',
|
|
1302
|
+
choices: [{ message: { content: 'Response' } }],
|
|
1303
|
+
usage: {},
|
|
1304
|
+
});
|
|
1305
|
+
};
|
|
1306
|
+
const model = new OpenRouterModel({
|
|
1307
|
+
settingsService: mockSettingsService,
|
|
1308
|
+
loggingService: logger,
|
|
1309
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
1310
|
+
});
|
|
1311
|
+
await model.getResponse({
|
|
1312
|
+
systemInstructions: 'You are helpful.',
|
|
1313
|
+
input: 'What is 2+2?',
|
|
1314
|
+
});
|
|
1315
|
+
t.truthy(requests[0]);
|
|
1316
|
+
const messages = requests[0].messages;
|
|
1317
|
+
// Verify we use exactly 2 cache points: system + last user
|
|
1318
|
+
const systemMsg = messages[0];
|
|
1319
|
+
const userMsg = messages[1];
|
|
1320
|
+
t.is(systemMsg.role, 'system');
|
|
1321
|
+
t.truthy(systemMsg.content[0].cache_control, 'System message should have cache_control');
|
|
1322
|
+
t.deepEqual(systemMsg.content[0].cache_control, { type: 'ephemeral' });
|
|
1323
|
+
t.is(userMsg.role, 'user');
|
|
1324
|
+
t.truthy(userMsg.content[0].cache_control, 'Last user message should have cache_control');
|
|
1325
|
+
t.deepEqual(userMsg.content[0].cache_control, { type: 'ephemeral' });
|
|
1326
|
+
});
|
|
1327
|
+
test.serial('applies cache_control to last user message in streamed response for Anthropic', async (t) => {
|
|
1328
|
+
const requests = [];
|
|
1329
|
+
const sseData = [
|
|
1330
|
+
'data: {"id":"resp-1","choices":[{"delta":{"content":"Hi"}}]}\n\n',
|
|
1331
|
+
'data: {"id":"resp-1","choices":[{"delta":{"content":"!"}}],"usage":{"prompt_tokens":10,"completion_tokens":2}}\n\n',
|
|
1332
|
+
'data: [DONE]\n\n',
|
|
1333
|
+
].join('');
|
|
1334
|
+
globalThis.fetch = async (_url, options) => {
|
|
1335
|
+
const body = JSON.parse(options.body);
|
|
1336
|
+
requests.push(body);
|
|
1337
|
+
const stream = new ReadableStream({
|
|
1338
|
+
start(controller) {
|
|
1339
|
+
controller.enqueue(new TextEncoder().encode(sseData));
|
|
1340
|
+
controller.close();
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
return new Response(stream, {
|
|
1344
|
+
status: 200,
|
|
1345
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
const model = new OpenRouterModel({
|
|
1349
|
+
settingsService: mockSettingsService,
|
|
1350
|
+
loggingService: logger,
|
|
1351
|
+
modelId: 'anthropic/claude-3.5-sonnet',
|
|
1352
|
+
});
|
|
1353
|
+
for await (const _event of model.getStreamedResponse({
|
|
1354
|
+
systemInstructions: 'You are helpful.',
|
|
1355
|
+
input: 'Hi there',
|
|
1356
|
+
})) {
|
|
1357
|
+
// Consume stream
|
|
1358
|
+
}
|
|
1359
|
+
t.truthy(requests[0]);
|
|
1360
|
+
// Verify both system and user message have cache_control in streamed response
|
|
1361
|
+
t.deepEqual(requests[0].messages[0], {
|
|
1362
|
+
role: 'system',
|
|
1363
|
+
content: [
|
|
1364
|
+
{
|
|
1365
|
+
type: 'text',
|
|
1366
|
+
text: 'You are helpful.',
|
|
1367
|
+
cache_control: { type: 'ephemeral' },
|
|
1368
|
+
},
|
|
1369
|
+
],
|
|
1370
|
+
});
|
|
1371
|
+
t.deepEqual(requests[0].messages[1], {
|
|
1372
|
+
role: 'user',
|
|
1373
|
+
content: [
|
|
1374
|
+
{
|
|
1375
|
+
type: 'text',
|
|
1376
|
+
text: 'Hi there',
|
|
1377
|
+
cache_control: { type: 'ephemeral' },
|
|
1378
|
+
},
|
|
1379
|
+
],
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
//# sourceMappingURL=openrouter.test.js.map
|