@calliopelabs/cli 0.8.20 → 2.0.2
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/dist/agents/agent-config-loader.d.ts +60 -0
- package/dist/agents/agent-config-loader.d.ts.map +1 -0
- package/dist/agents/agent-config-loader.js +402 -0
- package/dist/agents/agent-config-loader.js.map +1 -0
- package/dist/agents/agent-config-presets.d.ts +10 -0
- package/dist/agents/agent-config-presets.d.ts.map +1 -0
- package/dist/agents/agent-config-presets.js +940 -0
- package/dist/agents/agent-config-presets.js.map +1 -0
- package/dist/agents/agent-config-types.d.ts +145 -0
- package/dist/agents/agent-config-types.d.ts.map +1 -0
- package/dist/agents/agent-config-types.js +12 -0
- package/dist/agents/agent-config-types.js.map +1 -0
- package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
- package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
- package/dist/{agterm → agents}/agent-detection.js +21 -5
- package/dist/agents/agent-detection.js.map +1 -0
- package/dist/agents/aggregator.d.ts +19 -0
- package/dist/agents/aggregator.d.ts.map +1 -0
- package/dist/agents/aggregator.js +141 -0
- package/dist/agents/aggregator.js.map +1 -0
- package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
- package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
- package/dist/{agterm → agents}/cli-backend.js +90 -12
- package/dist/agents/cli-backend.js.map +1 -0
- package/dist/agents/council-types.d.ts +113 -0
- package/dist/agents/council-types.d.ts.map +1 -0
- package/dist/agents/council-types.js +81 -0
- package/dist/agents/council-types.js.map +1 -0
- package/dist/agents/council.d.ts +107 -0
- package/dist/agents/council.d.ts.map +1 -0
- package/dist/agents/council.js +586 -0
- package/dist/agents/council.js.map +1 -0
- package/dist/agents/decomposer.d.ts +33 -0
- package/dist/agents/decomposer.d.ts.map +1 -0
- package/dist/agents/decomposer.js +138 -0
- package/dist/agents/decomposer.js.map +1 -0
- package/dist/agents/dynamic-tools.d.ts +52 -0
- package/dist/agents/dynamic-tools.d.ts.map +1 -0
- package/dist/agents/dynamic-tools.js +395 -0
- package/dist/agents/dynamic-tools.js.map +1 -0
- package/dist/agents/index.d.ts +29 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +29 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/installer.d.ts +39 -0
- package/dist/agents/installer.d.ts.map +1 -0
- package/dist/agents/installer.js +205 -0
- package/dist/agents/installer.js.map +1 -0
- package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/{agterm → agents}/orchestrator.js +22 -2
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/sdk-backend.d.ts +63 -0
- package/dist/agents/sdk-backend.d.ts.map +1 -0
- package/dist/agents/sdk-backend.js +489 -0
- package/dist/agents/sdk-backend.js.map +1 -0
- package/dist/agents/swarm-types.d.ts +83 -0
- package/dist/agents/swarm-types.d.ts.map +1 -0
- package/dist/agents/swarm-types.js +20 -0
- package/dist/agents/swarm-types.js.map +1 -0
- package/dist/agents/swarm.d.ts +74 -0
- package/dist/agents/swarm.d.ts.map +1 -0
- package/dist/agents/swarm.js +307 -0
- package/dist/agents/swarm.js.map +1 -0
- package/dist/{agterm → agents}/tools.d.ts +7 -5
- package/dist/agents/tools.d.ts.map +1 -0
- package/dist/agents/tools.js +776 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/{agterm → agents}/types.d.ts +14 -2
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/{agterm → agents}/types.js +2 -2
- package/dist/agents/types.js.map +1 -0
- package/dist/api-server.d.ts +26 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +230 -0
- package/dist/api-server.js.map +1 -0
- package/dist/auto-checkpoint.d.ts +35 -0
- package/dist/auto-checkpoint.d.ts.map +1 -0
- package/dist/auto-checkpoint.js +143 -0
- package/dist/auto-checkpoint.js.map +1 -0
- package/dist/auto-compressor.d.ts +44 -0
- package/dist/auto-compressor.d.ts.map +1 -0
- package/dist/auto-compressor.js +145 -0
- package/dist/auto-compressor.js.map +1 -0
- package/dist/background-jobs.d.ts +45 -0
- package/dist/background-jobs.d.ts.map +1 -0
- package/dist/background-jobs.js +122 -0
- package/dist/background-jobs.js.map +1 -0
- package/dist/bin.d.ts +6 -2
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +127 -24
- package/dist/bin.js.map +1 -1
- package/dist/checkpoint.d.ts +49 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +219 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/circuit-breaker/breaker.d.ts +80 -0
- package/dist/circuit-breaker/breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/breaker.js +408 -0
- package/dist/circuit-breaker/breaker.js.map +1 -0
- package/dist/circuit-breaker/defaults.d.ts +8 -0
- package/dist/circuit-breaker/defaults.d.ts.map +1 -0
- package/dist/circuit-breaker/defaults.js +35 -0
- package/dist/circuit-breaker/defaults.js.map +1 -0
- package/dist/circuit-breaker/index.d.ts +9 -0
- package/dist/circuit-breaker/index.d.ts.map +1 -0
- package/dist/circuit-breaker/index.js +8 -0
- package/dist/circuit-breaker/index.js.map +1 -0
- package/dist/circuit-breaker/types.d.ts +77 -0
- package/dist/circuit-breaker/types.d.ts.map +1 -0
- package/dist/circuit-breaker/types.js +8 -0
- package/dist/circuit-breaker/types.js.map +1 -0
- package/dist/circuit-breaker.d.ts +8 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +7 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/cli/agent.d.ts +9 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +262 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/commands.d.ts +12 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/{cli.js → cli/commands.js} +285 -422
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +222 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +30 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +20 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/companions.d.ts +54 -0
- package/dist/companions.d.ts.map +1 -0
- package/dist/companions.js +440 -0
- package/dist/companions.js.map +1 -0
- package/dist/config.d.ts +23 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +95 -22
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +27 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +415 -10
- package/dist/diff.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -11
- package/dist/errors.js.map +1 -1
- package/dist/git-status.d.ts +23 -0
- package/dist/git-status.d.ts.map +1 -0
- package/dist/git-status.js +92 -0
- package/dist/git-status.js.map +1 -0
- package/dist/headless.d.ts +25 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +182 -0
- package/dist/headless.js.map +1 -0
- package/dist/hud/api.d.ts +35 -0
- package/dist/hud/api.d.ts.map +1 -0
- package/dist/hud/api.js +448 -0
- package/dist/hud/api.js.map +1 -0
- package/dist/hud/palettes.d.ts +9 -0
- package/dist/hud/palettes.d.ts.map +1 -0
- package/dist/hud/palettes.js +280 -0
- package/dist/hud/palettes.js.map +1 -0
- package/dist/hud/skins.d.ts +12 -0
- package/dist/hud/skins.d.ts.map +1 -0
- package/dist/hud/skins.js +365 -0
- package/dist/hud/skins.js.map +1 -0
- package/dist/hud/theme-packs/api.d.ts +51 -0
- package/dist/hud/theme-packs/api.d.ts.map +1 -0
- package/dist/hud/theme-packs/api.js +145 -0
- package/dist/hud/theme-packs/api.js.map +1 -0
- package/dist/hud/theme-packs/index.d.ts +18 -0
- package/dist/hud/theme-packs/index.d.ts.map +1 -0
- package/dist/hud/theme-packs/index.js +38 -0
- package/dist/hud/theme-packs/index.js.map +1 -0
- package/dist/hud/theme-packs/types.d.ts +29 -0
- package/dist/hud/theme-packs/types.d.ts.map +1 -0
- package/dist/hud/theme-packs/types.js +9 -0
- package/dist/hud/theme-packs/types.js.map +1 -0
- package/dist/hud/types.d.ts +182 -0
- package/dist/hud/types.d.ts.map +1 -0
- package/dist/hud/types.js +7 -0
- package/dist/hud/types.js.map +1 -0
- package/dist/idle-eviction.d.ts +34 -0
- package/dist/idle-eviction.d.ts.map +1 -0
- package/dist/idle-eviction.js +78 -0
- package/dist/idle-eviction.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/iteration-ledger.d.ts +105 -0
- package/dist/iteration-ledger.d.ts.map +1 -0
- package/dist/iteration-ledger.js +237 -0
- package/dist/iteration-ledger.js.map +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +1 -27
- package/dist/markdown.js.map +1 -1
- package/dist/mcp.d.ts +35 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +291 -7
- package/dist/mcp.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +12 -2
- package/dist/memory.js.map +1 -1
- package/dist/model-detection.d.ts +5 -0
- package/dist/model-detection.d.ts.map +1 -1
- package/dist/model-detection.js +278 -10
- package/dist/model-detection.js.map +1 -1
- package/dist/model-router.d.ts.map +1 -1
- package/dist/model-router.js +33 -11
- package/dist/model-router.js.map +1 -1
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +97 -6
- package/dist/plugins.js.map +1 -1
- package/dist/providers/anthropic.d.ts +10 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +221 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +17 -0
- package/dist/providers/bedrock.d.ts.map +1 -0
- package/dist/providers/bedrock.js +574 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/compat.d.ts +13 -0
- package/dist/providers/compat.d.ts.map +1 -0
- package/dist/providers/compat.js +202 -0
- package/dist/providers/compat.js.map +1 -0
- package/dist/providers/google.d.ts +10 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +203 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +23 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +145 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +17 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +289 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +121 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +485 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +63 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +164 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/sandbox-native.d.ts +59 -0
- package/dist/sandbox-native.d.ts.map +1 -0
- package/dist/sandbox-native.js +292 -0
- package/dist/sandbox-native.js.map +1 -0
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +59 -13
- package/dist/sandbox.js.map +1 -1
- package/dist/scope.d.ts +3 -1
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +13 -1
- package/dist/scope.js.map +1 -1
- package/dist/session-timeout.d.ts +31 -0
- package/dist/session-timeout.d.ts.map +1 -0
- package/dist/session-timeout.js +100 -0
- package/dist/session-timeout.js.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +29 -17
- package/dist/setup.js.map +1 -1
- package/dist/smart-router.d.ts +73 -0
- package/dist/smart-router.d.ts.map +1 -0
- package/dist/smart-router.js +332 -0
- package/dist/smart-router.js.map +1 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +164 -1
- package/dist/storage.js.map +1 -1
- package/dist/streaming.d.ts +4 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +12 -0
- package/dist/streaming.js.map +1 -1
- package/dist/styles.d.ts +32 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +91 -0
- package/dist/styles.js.map +1 -1
- package/dist/summarization.d.ts +1 -1
- package/dist/summarization.js +4 -4
- package/dist/summarization.js.map +1 -1
- package/dist/terminal-image.d.ts +115 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +766 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal-recording.d.ts +55 -0
- package/dist/terminal-recording.d.ts.map +1 -0
- package/dist/terminal-recording.js +182 -0
- package/dist/terminal-recording.js.map +1 -0
- package/dist/themes.d.ts +19 -35
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +101 -210
- package/dist/themes.js.map +1 -1
- package/dist/tmux.d.ts +35 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +106 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +587 -45
- package/dist/tools.js.map +1 -1
- package/dist/trust.d.ts +53 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +154 -0
- package/dist/trust.js.map +1 -0
- package/dist/types.d.ts +7 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +70 -32
- package/dist/types.js.map +1 -1
- package/dist/ui/agent.d.ts +61 -0
- package/dist/ui/agent.d.ts.map +1 -0
- package/dist/ui/agent.js +768 -0
- package/dist/ui/agent.js.map +1 -0
- package/dist/ui/chat-input.d.ts +32 -0
- package/dist/ui/chat-input.d.ts.map +1 -0
- package/dist/ui/chat-input.js +355 -0
- package/dist/ui/chat-input.js.map +1 -0
- package/dist/ui/commands.d.ts +92 -0
- package/dist/ui/commands.d.ts.map +1 -0
- package/dist/ui/commands.js +3006 -0
- package/dist/ui/commands.js.map +1 -0
- package/dist/ui/completions.d.ts +22 -0
- package/dist/ui/completions.d.ts.map +1 -0
- package/dist/ui/completions.js +215 -0
- package/dist/ui/completions.js.map +1 -0
- package/dist/ui/components.d.ts +38 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +422 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/context.d.ts +12 -0
- package/dist/ui/context.d.ts.map +1 -0
- package/dist/ui/context.js +102 -0
- package/dist/ui/context.js.map +1 -0
- package/dist/ui/error-boundary.d.ts +33 -0
- package/dist/ui/error-boundary.d.ts.map +1 -0
- package/dist/ui/error-boundary.js +94 -0
- package/dist/ui/error-boundary.js.map +1 -0
- package/dist/ui/frame.d.ts +13 -0
- package/dist/ui/frame.d.ts.map +1 -0
- package/dist/ui/frame.js +89 -0
- package/dist/ui/frame.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +928 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/messages.d.ts +19 -0
- package/dist/ui/messages.d.ts.map +1 -0
- package/dist/ui/messages.js +181 -0
- package/dist/ui/messages.js.map +1 -0
- package/dist/ui/modals.d.ts +52 -0
- package/dist/ui/modals.d.ts.map +1 -0
- package/dist/ui/modals.js +204 -0
- package/dist/ui/modals.js.map +1 -0
- package/dist/ui/pack-picker.d.ts +12 -0
- package/dist/ui/pack-picker.d.ts.map +1 -0
- package/dist/ui/pack-picker.js +101 -0
- package/dist/ui/pack-picker.js.map +1 -0
- package/dist/ui/status-bar.d.ts +20 -0
- package/dist/ui/status-bar.d.ts.map +1 -0
- package/dist/ui/status-bar.js +41 -0
- package/dist/ui/status-bar.js.map +1 -0
- package/dist/ui/theme-picker.d.ts +24 -0
- package/dist/ui/theme-picker.d.ts.map +1 -0
- package/dist/ui/theme-picker.js +190 -0
- package/dist/ui/theme-picker.js.map +1 -0
- package/dist/ui/types.d.ts +62 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +7 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -9
- package/dist/version-check.js.map +1 -1
- package/package.json +8 -3
- package/dist/agterm/agent-detection.js.map +0 -1
- package/dist/agterm/cli-backend.js.map +0 -1
- package/dist/agterm/index.d.ts +0 -12
- package/dist/agterm/index.d.ts.map +0 -1
- package/dist/agterm/index.js +0 -15
- package/dist/agterm/index.js.map +0 -1
- package/dist/agterm/orchestrator.d.ts.map +0 -1
- package/dist/agterm/orchestrator.js.map +0 -1
- package/dist/agterm/tools.d.ts.map +0 -1
- package/dist/agterm/tools.js +0 -278
- package/dist/agterm/tools.js.map +0 -1
- package/dist/agterm/types.d.ts.map +0 -1
- package/dist/agterm/types.js.map +0 -1
- package/dist/cli.d.ts +0 -14
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/providers.d.ts +0 -51
- package/dist/providers.d.ts.map +0 -1
- package/dist/providers.js +0 -1146
- package/dist/providers.js.map +0 -1
- package/dist/ui-cli.d.ts +0 -17
- package/dist/ui-cli.d.ts.map +0 -1
- package/dist/ui-cli.js +0 -3730
- package/dist/ui-cli.js.map +0 -1
package/dist/providers.js
DELETED
|
@@ -1,1146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calliope CLI - LLM Providers
|
|
3
|
-
*
|
|
4
|
-
* Handles communication with different LLM providers.
|
|
5
|
-
*/
|
|
6
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
-
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
8
|
-
import OpenAI from 'openai';
|
|
9
|
-
import * as config from './config.js';
|
|
10
|
-
import { withRetry } from './errors.js';
|
|
11
|
-
import { getModelContextLimit } from './model-detection.js';
|
|
12
|
-
import { DEFAULT_MODELS } from './types.js';
|
|
13
|
-
/**
|
|
14
|
-
* Extract text from MessageContent
|
|
15
|
-
*/
|
|
16
|
-
function getTextContent(content) {
|
|
17
|
-
if (typeof content === 'string') {
|
|
18
|
-
return content;
|
|
19
|
-
}
|
|
20
|
-
return content
|
|
21
|
-
.filter(block => block.type === 'text')
|
|
22
|
-
.map(block => block.text)
|
|
23
|
-
.join('\n');
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Convert MessageContent to Anthropic content format
|
|
27
|
-
*/
|
|
28
|
-
function toAnthropicContent(content) {
|
|
29
|
-
if (typeof content === 'string') {
|
|
30
|
-
return content;
|
|
31
|
-
}
|
|
32
|
-
return content.map(block => {
|
|
33
|
-
if (block.type === 'text') {
|
|
34
|
-
return { type: 'text', text: block.text };
|
|
35
|
-
}
|
|
36
|
-
else if (block.type === 'image') {
|
|
37
|
-
return {
|
|
38
|
-
type: 'image',
|
|
39
|
-
source: {
|
|
40
|
-
type: 'base64',
|
|
41
|
-
media_type: block.mediaType,
|
|
42
|
-
data: block.data,
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return { type: 'text', text: '' };
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Convert MessageContent to OpenAI content format
|
|
51
|
-
*/
|
|
52
|
-
function toOpenAIContent(content) {
|
|
53
|
-
if (typeof content === 'string') {
|
|
54
|
-
return content;
|
|
55
|
-
}
|
|
56
|
-
return content.map(block => {
|
|
57
|
-
if (block.type === 'text') {
|
|
58
|
-
return { type: 'text', text: block.text };
|
|
59
|
-
}
|
|
60
|
-
else if (block.type === 'image') {
|
|
61
|
-
return {
|
|
62
|
-
type: 'image_url',
|
|
63
|
-
image_url: {
|
|
64
|
-
url: `data:${block.mediaType};base64,${block.data}`,
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return { type: 'text', text: '' };
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
// Constants
|
|
72
|
-
const MAX_TOKENS = 8192;
|
|
73
|
-
const MIN_OUTPUT_TOKENS = 1024; // Minimum output tokens to request
|
|
74
|
-
const CONTEXT_BUFFER_PERCENT = 0.08; // 8% of context as safety buffer
|
|
75
|
-
const CONTEXT_BUFFER_MIN = 5000; // Minimum 5k buffer
|
|
76
|
-
// Debug logging helper
|
|
77
|
-
const DEBUG = process.env.CALLIOPE_DEBUG === '1';
|
|
78
|
-
function debugLog(message, ...args) {
|
|
79
|
-
if (DEBUG)
|
|
80
|
-
console.log(`[DEBUG] ${message}`, ...args);
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Estimate tokens from messages (conservative: ~3 chars per token)
|
|
84
|
-
* Uses conservative estimation to avoid context overflow
|
|
85
|
-
*/
|
|
86
|
-
function estimateInputTokens(messages, tools) {
|
|
87
|
-
let totalChars = 0;
|
|
88
|
-
for (const msg of messages) {
|
|
89
|
-
// Add per-message overhead (role, structure, etc.)
|
|
90
|
-
totalChars += 50;
|
|
91
|
-
if (typeof msg.content === 'string') {
|
|
92
|
-
totalChars += msg.content.length;
|
|
93
|
-
}
|
|
94
|
-
else if (Array.isArray(msg.content)) {
|
|
95
|
-
for (const block of msg.content) {
|
|
96
|
-
if (block.type === 'text') {
|
|
97
|
-
totalChars += block.text.length;
|
|
98
|
-
}
|
|
99
|
-
else if (block.type === 'image') {
|
|
100
|
-
// Images are roughly 85 tokens per tile (assuming ~750 tokens average)
|
|
101
|
-
totalChars += 3000;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Add overhead for tool calls in assistant messages
|
|
106
|
-
if (msg.toolCalls) {
|
|
107
|
-
totalChars += JSON.stringify(msg.toolCalls).length;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Add tool definitions overhead
|
|
111
|
-
if (tools.length > 0) {
|
|
112
|
-
totalChars += JSON.stringify(tools).length;
|
|
113
|
-
}
|
|
114
|
-
// Very conservative estimate: 2.5 characters per token
|
|
115
|
-
// Plus 35% overhead for message structure, system prompt, and formatting
|
|
116
|
-
return Math.ceil((totalChars / 2.5) * 1.35);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Calculate dynamic max_tokens based on available context space
|
|
120
|
-
*/
|
|
121
|
-
function calculateMaxTokens(provider, model, messages, tools) {
|
|
122
|
-
const contextLimit = getModelContextLimit(provider, model);
|
|
123
|
-
const estimatedInput = estimateInputTokens(messages, tools);
|
|
124
|
-
// Use percentage-based buffer with minimum floor
|
|
125
|
-
const buffer = Math.max(CONTEXT_BUFFER_MIN, Math.ceil(contextLimit * CONTEXT_BUFFER_PERCENT));
|
|
126
|
-
const available = contextLimit - estimatedInput - buffer;
|
|
127
|
-
debugLog(`Context calculation: limit=${contextLimit}, input≈${estimatedInput}, buffer=${buffer}, available=${available}`);
|
|
128
|
-
// Ensure we have at least MIN_OUTPUT_TOKENS, up to MAX_TOKENS
|
|
129
|
-
if (available < MIN_OUTPUT_TOKENS) {
|
|
130
|
-
debugLog(`WARNING: Very limited output space (${available}), using minimum ${MIN_OUTPUT_TOKENS}`);
|
|
131
|
-
return MIN_OUTPUT_TOKENS;
|
|
132
|
-
}
|
|
133
|
-
return Math.min(MAX_TOKENS, available);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Check if context needs summarization based on actual token usage
|
|
137
|
-
* Call this with the input_tokens from the last API response
|
|
138
|
-
*/
|
|
139
|
-
export function needsSummarization(provider, model, actualInputTokens) {
|
|
140
|
-
const contextLimit = getModelContextLimit(provider, model);
|
|
141
|
-
const threshold = contextLimit * 0.85; // Trigger summarization at 85% full
|
|
142
|
-
return actualInputTokens >= threshold;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Get context health info
|
|
146
|
-
*/
|
|
147
|
-
export function getContextHealth(provider, model, actualInputTokens) {
|
|
148
|
-
const limit = getModelContextLimit(provider, model);
|
|
149
|
-
const percent = Math.round((actualInputTokens / limit) * 100);
|
|
150
|
-
return {
|
|
151
|
-
limit,
|
|
152
|
-
used: actualInputTokens,
|
|
153
|
-
percent,
|
|
154
|
-
needsSummarization: actualInputTokens >= limit * 0.85,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Estimate context usage before making a request (for pre-request summarization)
|
|
159
|
-
* Uses conservative estimation since we don't have actual token counts yet
|
|
160
|
-
*/
|
|
161
|
-
export function estimateContextUsage(provider, model, messages, tools) {
|
|
162
|
-
const estimated = estimateInputTokens(messages, tools);
|
|
163
|
-
const limit = getModelContextLimit(provider, model);
|
|
164
|
-
const percent = Math.round((estimated / limit) * 100);
|
|
165
|
-
return {
|
|
166
|
-
estimated,
|
|
167
|
-
limit,
|
|
168
|
-
percent,
|
|
169
|
-
needsSummarization: estimated >= limit * 0.80, // More aggressive threshold for estimates
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
// ============================================================================
|
|
173
|
-
// LLM Response Validation
|
|
174
|
-
// ============================================================================
|
|
175
|
-
/** Maximum allowed content length (1MB) to prevent memory issues */
|
|
176
|
-
const MAX_CONTENT_LENGTH = 1024 * 1024;
|
|
177
|
-
/**
|
|
178
|
-
* Validate and sanitize LLM response
|
|
179
|
-
*/
|
|
180
|
-
function validateLLMResponse(response) {
|
|
181
|
-
// Ensure content is a string
|
|
182
|
-
if (response.content === null || response.content === undefined) {
|
|
183
|
-
response.content = '';
|
|
184
|
-
}
|
|
185
|
-
else if (typeof response.content !== 'string') {
|
|
186
|
-
response.content = String(response.content);
|
|
187
|
-
}
|
|
188
|
-
// Truncate if too long to prevent memory issues
|
|
189
|
-
if (response.content.length > MAX_CONTENT_LENGTH) {
|
|
190
|
-
debugLog('Response content truncated from', response.content.length, 'to', MAX_CONTENT_LENGTH);
|
|
191
|
-
response.content = response.content.slice(0, MAX_CONTENT_LENGTH) + '\n... [truncated]';
|
|
192
|
-
}
|
|
193
|
-
// Validate tool calls if present
|
|
194
|
-
if (response.toolCalls) {
|
|
195
|
-
response.toolCalls = response.toolCalls.filter(call => {
|
|
196
|
-
if (!call.id || typeof call.id !== 'string') {
|
|
197
|
-
debugLog('Invalid tool call: missing or invalid id', call);
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
if (!call.name || typeof call.name !== 'string') {
|
|
201
|
-
debugLog('Invalid tool call: missing or invalid name', call);
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
if (call.arguments === null || call.arguments === undefined) {
|
|
205
|
-
call.arguments = {};
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
|
-
});
|
|
209
|
-
if (response.toolCalls.length === 0) {
|
|
210
|
-
response.toolCalls = undefined;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
// Ensure valid finish reason
|
|
214
|
-
if (!['stop', 'tool_use', 'length', 'error'].includes(response.finishReason)) {
|
|
215
|
-
response.finishReason = 'stop';
|
|
216
|
-
}
|
|
217
|
-
return response;
|
|
218
|
-
}
|
|
219
|
-
// ============================================================================
|
|
220
|
-
/**
|
|
221
|
-
* Models that require the Responses API instead of Chat Completions
|
|
222
|
-
* These are OpenAI's newer reasoning models that only work with /v1/responses
|
|
223
|
-
*/
|
|
224
|
-
const RESPONSES_API_MODELS = [
|
|
225
|
-
'o3',
|
|
226
|
-
'o3-mini',
|
|
227
|
-
'o3-pro',
|
|
228
|
-
'o4-mini',
|
|
229
|
-
'gpt-5',
|
|
230
|
-
];
|
|
231
|
-
/**
|
|
232
|
-
* Check if a model requires the Responses API
|
|
233
|
-
*/
|
|
234
|
-
function requiresResponsesAPI(model) {
|
|
235
|
-
return RESPONSES_API_MODELS.some(m => model.startsWith(m));
|
|
236
|
-
}
|
|
237
|
-
// API base URLs for OpenAI-compatible providers
|
|
238
|
-
const PROVIDER_BASE_URLS = {
|
|
239
|
-
openrouter: 'https://openrouter.ai/api/v1',
|
|
240
|
-
together: 'https://api.together.xyz/v1',
|
|
241
|
-
groq: 'https://api.groq.com/openai/v1',
|
|
242
|
-
fireworks: 'https://api.fireworks.ai/inference/v1',
|
|
243
|
-
mistral: 'https://api.mistral.ai/v1',
|
|
244
|
-
ai21: 'https://api.ai21.com/studio/v1',
|
|
245
|
-
huggingface: 'https://api-inference.huggingface.co/v1',
|
|
246
|
-
};
|
|
247
|
-
/**
|
|
248
|
-
* Convert messages to OpenAI format
|
|
249
|
-
*/
|
|
250
|
-
function toOpenAIMessages(messages) {
|
|
251
|
-
return messages.map(m => {
|
|
252
|
-
if (m.role === 'tool') {
|
|
253
|
-
return {
|
|
254
|
-
role: 'tool',
|
|
255
|
-
tool_call_id: m.toolCallId || '',
|
|
256
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
260
|
-
return {
|
|
261
|
-
role: 'assistant',
|
|
262
|
-
content: typeof m.content === 'string' ? m.content : (m.content ? JSON.stringify(m.content) : null),
|
|
263
|
-
tool_calls: m.toolCalls.map(tc => ({
|
|
264
|
-
id: tc.id,
|
|
265
|
-
type: 'function',
|
|
266
|
-
function: {
|
|
267
|
-
name: tc.name,
|
|
268
|
-
arguments: JSON.stringify(tc.arguments),
|
|
269
|
-
},
|
|
270
|
-
})),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
// Handle multi-modal content for user messages
|
|
274
|
-
if (m.role === 'user' && Array.isArray(m.content)) {
|
|
275
|
-
return {
|
|
276
|
-
role: 'user',
|
|
277
|
-
content: toOpenAIContent(m.content),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
role: m.role,
|
|
282
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
283
|
-
};
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Convert tools to OpenAI format
|
|
288
|
-
*/
|
|
289
|
-
function toOpenAITools(tools) {
|
|
290
|
-
return tools.map(t => ({
|
|
291
|
-
type: 'function',
|
|
292
|
-
function: {
|
|
293
|
-
name: t.name,
|
|
294
|
-
description: t.description,
|
|
295
|
-
parameters: t.parameters,
|
|
296
|
-
},
|
|
297
|
-
}));
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Parse tool calls from OpenAI response
|
|
301
|
-
*/
|
|
302
|
-
function parseOpenAIToolCalls(toolCalls) {
|
|
303
|
-
if (!toolCalls)
|
|
304
|
-
return [];
|
|
305
|
-
const result = [];
|
|
306
|
-
for (const tc of toolCalls) {
|
|
307
|
-
let parsedArgs = {};
|
|
308
|
-
try {
|
|
309
|
-
parsedArgs = JSON.parse(tc.function.arguments);
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
const parseError = error instanceof SyntaxError ? error.message : 'Unknown parse error';
|
|
313
|
-
throw new Error(`Invalid tool arguments from LLM: ${parseError}. Raw: ${tc.function.arguments.substring(0, 200)}`);
|
|
314
|
-
}
|
|
315
|
-
result.push({
|
|
316
|
-
id: tc.id,
|
|
317
|
-
name: tc.function.name,
|
|
318
|
-
arguments: parsedArgs,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Get available providers based on configured API keys
|
|
325
|
-
*/
|
|
326
|
-
export function getAvailableProviders() {
|
|
327
|
-
const providers = [];
|
|
328
|
-
if (config.getApiKey('anthropic'))
|
|
329
|
-
providers.push('anthropic');
|
|
330
|
-
if (config.getApiKey('google'))
|
|
331
|
-
providers.push('google');
|
|
332
|
-
if (config.getApiKey('openai'))
|
|
333
|
-
providers.push('openai');
|
|
334
|
-
if (config.getApiKey('openrouter'))
|
|
335
|
-
providers.push('openrouter');
|
|
336
|
-
if (config.getApiKey('together'))
|
|
337
|
-
providers.push('together');
|
|
338
|
-
if (config.getApiKey('groq'))
|
|
339
|
-
providers.push('groq');
|
|
340
|
-
if (config.getApiKey('mistral'))
|
|
341
|
-
providers.push('mistral');
|
|
342
|
-
if (config.getBaseUrl('ollama'))
|
|
343
|
-
providers.push('ollama');
|
|
344
|
-
if (config.getApiKey('ai21'))
|
|
345
|
-
providers.push('ai21');
|
|
346
|
-
if (config.getApiKey('huggingface'))
|
|
347
|
-
providers.push('huggingface');
|
|
348
|
-
if (config.getBaseUrl('litellm'))
|
|
349
|
-
providers.push('litellm');
|
|
350
|
-
return providers;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Select the best available provider
|
|
354
|
-
*/
|
|
355
|
-
export function selectProvider(preferred) {
|
|
356
|
-
if (preferred !== 'auto') {
|
|
357
|
-
// For Ollama/LiteLLM, check base URL instead of API key
|
|
358
|
-
if (preferred === 'ollama' || preferred === 'litellm') {
|
|
359
|
-
if (config.getBaseUrl(preferred))
|
|
360
|
-
return preferred;
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
const key = config.getApiKey(preferred);
|
|
364
|
-
if (key)
|
|
365
|
-
return preferred;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Auto-select: prefer Anthropic > OpenAI > Google > others
|
|
369
|
-
const priority = ['anthropic', 'openai', 'google', 'mistral', 'openrouter', 'together', 'groq', 'ollama', 'litellm'];
|
|
370
|
-
for (const p of priority) {
|
|
371
|
-
if (p === 'ollama' || p === 'litellm') {
|
|
372
|
-
if (config.getBaseUrl(p))
|
|
373
|
-
return p;
|
|
374
|
-
}
|
|
375
|
-
else if (config.getApiKey(p)) {
|
|
376
|
-
return p;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
throw new Error('No API keys configured. Run `calliope --setup` to configure.');
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Chat with the selected provider (with automatic retry)
|
|
383
|
-
*/
|
|
384
|
-
export async function chat(provider, messages, tools, model, onToken, onRetry) {
|
|
385
|
-
const actualProvider = selectProvider(provider);
|
|
386
|
-
const actualModel = model || DEFAULT_MODELS[actualProvider];
|
|
387
|
-
const doChat = async () => {
|
|
388
|
-
let response;
|
|
389
|
-
switch (actualProvider) {
|
|
390
|
-
case 'anthropic':
|
|
391
|
-
response = await chatAnthropic(messages, tools, actualModel, onToken);
|
|
392
|
-
break;
|
|
393
|
-
case 'google':
|
|
394
|
-
response = await chatGoogle(messages, tools, actualModel);
|
|
395
|
-
break;
|
|
396
|
-
case 'openai':
|
|
397
|
-
response = await chatOpenAI(messages, tools, actualModel, onToken);
|
|
398
|
-
break;
|
|
399
|
-
case 'openrouter':
|
|
400
|
-
case 'together':
|
|
401
|
-
case 'groq':
|
|
402
|
-
case 'fireworks':
|
|
403
|
-
case 'mistral':
|
|
404
|
-
case 'ai21':
|
|
405
|
-
case 'huggingface':
|
|
406
|
-
case 'ollama':
|
|
407
|
-
case 'litellm':
|
|
408
|
-
response = await chatOpenAICompatible(actualProvider, messages, tools, actualModel, onToken);
|
|
409
|
-
break;
|
|
410
|
-
default:
|
|
411
|
-
throw new Error(`Provider ${actualProvider} not implemented`);
|
|
412
|
-
}
|
|
413
|
-
// Validate and sanitize response before returning
|
|
414
|
-
return validateLLMResponse(response);
|
|
415
|
-
};
|
|
416
|
-
// Wrap with retry logic
|
|
417
|
-
return withRetry(doChat, {
|
|
418
|
-
maxRetries: 2,
|
|
419
|
-
initialDelayMs: 1000,
|
|
420
|
-
onRetry: onRetry,
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Chat with Anthropic Claude
|
|
425
|
-
*/
|
|
426
|
-
async function chatAnthropic(messages, tools, model, onToken) {
|
|
427
|
-
const apiKey = config.getApiKey('anthropic');
|
|
428
|
-
if (!apiKey)
|
|
429
|
-
throw new Error('Anthropic API key not configured');
|
|
430
|
-
const client = new Anthropic({ apiKey });
|
|
431
|
-
// Extract system message
|
|
432
|
-
const systemMessage = messages.find(m => m.role === 'system');
|
|
433
|
-
const chatMessages = messages.filter(m => m.role !== 'system');
|
|
434
|
-
// Convert to Anthropic format
|
|
435
|
-
const anthropicMessages = chatMessages.map(m => {
|
|
436
|
-
if (m.role === 'tool') {
|
|
437
|
-
return {
|
|
438
|
-
role: 'user',
|
|
439
|
-
content: [{
|
|
440
|
-
type: 'tool_result',
|
|
441
|
-
tool_use_id: m.toolCallId || '',
|
|
442
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
443
|
-
}],
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
447
|
-
const textContent = typeof m.content === 'string' ? m.content :
|
|
448
|
-
(Array.isArray(m.content) ? m.content.filter(b => b.type === 'text').map(b => b.text).join('\n') : '');
|
|
449
|
-
return {
|
|
450
|
-
role: 'assistant',
|
|
451
|
-
content: [
|
|
452
|
-
...(textContent ? [{ type: 'text', text: textContent }] : []),
|
|
453
|
-
...m.toolCalls.map(tc => ({
|
|
454
|
-
type: 'tool_use',
|
|
455
|
-
id: tc.id,
|
|
456
|
-
name: tc.name,
|
|
457
|
-
input: tc.arguments,
|
|
458
|
-
})),
|
|
459
|
-
],
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
// Handle multi-modal content for user messages
|
|
463
|
-
if (m.role === 'user' && Array.isArray(m.content)) {
|
|
464
|
-
return {
|
|
465
|
-
role: 'user',
|
|
466
|
-
content: toAnthropicContent(m.content),
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
470
|
-
return {
|
|
471
|
-
role: m.role,
|
|
472
|
-
// Anthropic requires non-empty content for all non-final messages
|
|
473
|
-
content: content || '(continued)',
|
|
474
|
-
};
|
|
475
|
-
});
|
|
476
|
-
// Convert tools to Anthropic format
|
|
477
|
-
const anthropicTools = tools.map(t => ({
|
|
478
|
-
name: t.name,
|
|
479
|
-
description: t.description,
|
|
480
|
-
input_schema: t.parameters,
|
|
481
|
-
}));
|
|
482
|
-
// Calculate dynamic max_tokens based on available context space
|
|
483
|
-
const dynamicMaxTokens = calculateMaxTokens('anthropic', model, messages, tools);
|
|
484
|
-
debugLog(`Anthropic request: model=${model}, max_tokens=${dynamicMaxTokens}`);
|
|
485
|
-
// Use streaming if callback provided - handles both text and tool calls
|
|
486
|
-
if (onToken) {
|
|
487
|
-
let content = '';
|
|
488
|
-
let inputTokens = 0;
|
|
489
|
-
let outputTokens = 0;
|
|
490
|
-
const toolCalls = [];
|
|
491
|
-
let currentToolId = '';
|
|
492
|
-
let currentToolName = '';
|
|
493
|
-
let currentToolInput = '';
|
|
494
|
-
let finishReason = 'stop';
|
|
495
|
-
try {
|
|
496
|
-
const stream = await client.messages.stream({
|
|
497
|
-
model,
|
|
498
|
-
max_tokens: dynamicMaxTokens,
|
|
499
|
-
system: systemMessage ? getTextContent(systemMessage.content) : '',
|
|
500
|
-
messages: anthropicMessages,
|
|
501
|
-
tools: anthropicTools.length > 0 ? anthropicTools : undefined,
|
|
502
|
-
});
|
|
503
|
-
for await (const event of stream) {
|
|
504
|
-
if (event.type === 'content_block_start') {
|
|
505
|
-
if (event.content_block.type === 'tool_use') {
|
|
506
|
-
currentToolId = event.content_block.id;
|
|
507
|
-
currentToolName = event.content_block.name;
|
|
508
|
-
currentToolInput = '';
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
else if (event.type === 'content_block_delta') {
|
|
512
|
-
if (event.delta.type === 'text_delta') {
|
|
513
|
-
const text = event.delta.text;
|
|
514
|
-
content += text;
|
|
515
|
-
onToken(text);
|
|
516
|
-
}
|
|
517
|
-
else if (event.delta.type === 'input_json_delta') {
|
|
518
|
-
currentToolInput += event.delta.partial_json;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else if (event.type === 'content_block_stop') {
|
|
522
|
-
if (currentToolId && currentToolName) {
|
|
523
|
-
try {
|
|
524
|
-
toolCalls.push({
|
|
525
|
-
id: currentToolId,
|
|
526
|
-
name: currentToolName,
|
|
527
|
-
arguments: JSON.parse(currentToolInput || '{}'),
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
toolCalls.push({
|
|
532
|
-
id: currentToolId,
|
|
533
|
-
name: currentToolName,
|
|
534
|
-
arguments: {},
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
currentToolId = '';
|
|
538
|
-
currentToolName = '';
|
|
539
|
-
currentToolInput = '';
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
else if (event.type === 'message_delta') {
|
|
543
|
-
if (event.usage) {
|
|
544
|
-
outputTokens = event.usage.output_tokens;
|
|
545
|
-
}
|
|
546
|
-
if (event.delta.stop_reason === 'tool_use') {
|
|
547
|
-
finishReason = 'tool_use';
|
|
548
|
-
}
|
|
549
|
-
else if (event.delta.stop_reason === 'max_tokens') {
|
|
550
|
-
finishReason = 'length';
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
else if (event.type === 'message_start' && event.message.usage) {
|
|
554
|
-
inputTokens = event.message.usage.input_tokens;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return {
|
|
558
|
-
content,
|
|
559
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
560
|
-
finishReason,
|
|
561
|
-
usage: { inputTokens, outputTokens },
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
catch (streamError) {
|
|
565
|
-
// Fall back to non-streaming on error
|
|
566
|
-
console.error('Anthropic streaming failed, falling back:', streamError);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
// Non-streaming request
|
|
570
|
-
const response = await client.messages.create({
|
|
571
|
-
model,
|
|
572
|
-
max_tokens: dynamicMaxTokens,
|
|
573
|
-
system: systemMessage ? getTextContent(systemMessage.content) : '',
|
|
574
|
-
messages: anthropicMessages,
|
|
575
|
-
tools: anthropicTools.length > 0 ? anthropicTools : undefined,
|
|
576
|
-
});
|
|
577
|
-
// Parse response
|
|
578
|
-
let content = '';
|
|
579
|
-
const toolCalls = [];
|
|
580
|
-
for (const block of response.content) {
|
|
581
|
-
if (block.type === 'text') {
|
|
582
|
-
content += block.text;
|
|
583
|
-
}
|
|
584
|
-
else if (block.type === 'tool_use') {
|
|
585
|
-
toolCalls.push({
|
|
586
|
-
id: block.id,
|
|
587
|
-
name: block.name,
|
|
588
|
-
arguments: block.input,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
// Map Anthropic stop reasons to our finish reasons
|
|
593
|
-
let finishReason = 'stop';
|
|
594
|
-
if (response.stop_reason === 'tool_use') {
|
|
595
|
-
finishReason = 'tool_use';
|
|
596
|
-
}
|
|
597
|
-
else if (response.stop_reason === 'max_tokens') {
|
|
598
|
-
finishReason = 'length';
|
|
599
|
-
}
|
|
600
|
-
return {
|
|
601
|
-
content,
|
|
602
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
603
|
-
finishReason,
|
|
604
|
-
usage: {
|
|
605
|
-
inputTokens: response.usage.input_tokens,
|
|
606
|
-
outputTokens: response.usage.output_tokens,
|
|
607
|
-
},
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Chat with Google Gemini
|
|
612
|
-
*/
|
|
613
|
-
async function chatGoogle(messages, tools, model) {
|
|
614
|
-
const apiKey = config.getApiKey('google');
|
|
615
|
-
if (!apiKey)
|
|
616
|
-
throw new Error('Google API key not configured');
|
|
617
|
-
const genAI = new GoogleGenerativeAI(apiKey);
|
|
618
|
-
const genModel = genAI.getGenerativeModel({ model });
|
|
619
|
-
// Build history (exclude last message)
|
|
620
|
-
const history = messages.slice(0, -1).filter(m => m.role !== 'system').map(m => ({
|
|
621
|
-
role: m.role === 'assistant' ? 'model' : 'user',
|
|
622
|
-
parts: [{ text: getTextContent(m.content) }],
|
|
623
|
-
}));
|
|
624
|
-
if (messages.length === 0) {
|
|
625
|
-
throw new Error('No messages provided');
|
|
626
|
-
}
|
|
627
|
-
const lastMessage = messages[messages.length - 1];
|
|
628
|
-
const systemMessage = messages.find(m => m.role === 'system');
|
|
629
|
-
const chat = genModel.startChat({
|
|
630
|
-
history,
|
|
631
|
-
systemInstruction: systemMessage ? getTextContent(systemMessage.content) : undefined,
|
|
632
|
-
});
|
|
633
|
-
// Convert last message to Gemini format (with image support)
|
|
634
|
-
const lastMessageParts = [];
|
|
635
|
-
if (typeof lastMessage.content === 'string') {
|
|
636
|
-
lastMessageParts.push({ text: lastMessage.content });
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
for (const block of lastMessage.content) {
|
|
640
|
-
if (block.type === 'text') {
|
|
641
|
-
lastMessageParts.push({ text: block.text });
|
|
642
|
-
}
|
|
643
|
-
else if (block.type === 'image') {
|
|
644
|
-
lastMessageParts.push({
|
|
645
|
-
inlineData: {
|
|
646
|
-
mimeType: block.mediaType,
|
|
647
|
-
data: block.data,
|
|
648
|
-
},
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
const result = await chat.sendMessage(lastMessageParts);
|
|
654
|
-
const response = result.response;
|
|
655
|
-
const text = response.text();
|
|
656
|
-
// Check for function calls
|
|
657
|
-
const toolCalls = [];
|
|
658
|
-
const candidates = response.candidates || [];
|
|
659
|
-
for (const candidate of candidates) {
|
|
660
|
-
for (const part of candidate.content?.parts || []) {
|
|
661
|
-
if ('functionCall' in part && part.functionCall) {
|
|
662
|
-
toolCalls.push({
|
|
663
|
-
id: `gemini_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
664
|
-
name: part.functionCall.name,
|
|
665
|
-
arguments: part.functionCall.args,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
content: text,
|
|
672
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
673
|
-
finishReason: toolCalls.length > 0 ? 'tool_use' : 'stop',
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Convert messages to Responses API input format
|
|
678
|
-
*/
|
|
679
|
-
function toResponsesInput(messages) {
|
|
680
|
-
const input = [];
|
|
681
|
-
for (const m of messages) {
|
|
682
|
-
if (m.role === 'system') {
|
|
683
|
-
// System messages become developer messages in Responses API
|
|
684
|
-
input.push({
|
|
685
|
-
role: 'developer',
|
|
686
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
else if (m.role === 'tool') {
|
|
690
|
-
// Tool results become function_call_output items
|
|
691
|
-
input.push({
|
|
692
|
-
type: 'function_call_output',
|
|
693
|
-
call_id: m.toolCallId || '',
|
|
694
|
-
output: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
else if (m.role === 'assistant') {
|
|
698
|
-
// Assistant messages with tool calls
|
|
699
|
-
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
700
|
-
// First add any text content as a message
|
|
701
|
-
const textContent = typeof m.content === 'string' ? m.content :
|
|
702
|
-
(Array.isArray(m.content) ? m.content.filter(b => b.type === 'text').map(b => b.text).join('\n') : '');
|
|
703
|
-
if (textContent) {
|
|
704
|
-
input.push({
|
|
705
|
-
role: 'assistant',
|
|
706
|
-
content: textContent,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
// Then add each tool call as a function_call item
|
|
710
|
-
for (const tc of m.toolCalls) {
|
|
711
|
-
input.push({
|
|
712
|
-
type: 'function_call',
|
|
713
|
-
call_id: tc.id,
|
|
714
|
-
name: tc.name,
|
|
715
|
-
arguments: JSON.stringify(tc.arguments),
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
input.push({
|
|
721
|
-
role: 'assistant',
|
|
722
|
-
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
else if (m.role === 'user') {
|
|
727
|
-
// Handle multi-modal content for user messages
|
|
728
|
-
if (Array.isArray(m.content)) {
|
|
729
|
-
const parts = [];
|
|
730
|
-
for (const block of m.content) {
|
|
731
|
-
if (block.type === 'text') {
|
|
732
|
-
parts.push({ type: 'input_text', text: block.text });
|
|
733
|
-
}
|
|
734
|
-
else if (block.type === 'image') {
|
|
735
|
-
parts.push({
|
|
736
|
-
type: 'input_image',
|
|
737
|
-
image_url: { url: `data:${block.mediaType};base64,${block.data}` },
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
input.push({ role: 'user', content: parts });
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
input.push({
|
|
745
|
-
role: 'user',
|
|
746
|
-
content: m.content,
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
return input;
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Convert tools to Responses API format
|
|
755
|
-
*/
|
|
756
|
-
function toResponsesTools(tools) {
|
|
757
|
-
return tools.map(t => ({
|
|
758
|
-
type: 'function',
|
|
759
|
-
name: t.name,
|
|
760
|
-
description: t.description,
|
|
761
|
-
parameters: t.parameters,
|
|
762
|
-
strict: false,
|
|
763
|
-
}));
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Type guard for text delta events
|
|
767
|
-
*/
|
|
768
|
-
function isTextDeltaEvent(event) {
|
|
769
|
-
return event.type === 'response.output_text.delta';
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Type guard for function call done events
|
|
773
|
-
*/
|
|
774
|
-
function isFunctionCallDoneEvent(event) {
|
|
775
|
-
return event.type === 'response.function_call_arguments.done';
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Type guard for completed events
|
|
779
|
-
*/
|
|
780
|
-
function isCompletedEvent(event) {
|
|
781
|
-
return event.type === 'response.completed';
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Type guard for function call output items
|
|
785
|
-
*/
|
|
786
|
-
function isFunctionCallOutput(item) {
|
|
787
|
-
return item.type === 'function_call';
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Chat with OpenAI using the Responses API (for o3, o4-mini, etc.)
|
|
791
|
-
*/
|
|
792
|
-
async function chatOpenAIResponses(messages, tools, model, onToken) {
|
|
793
|
-
const apiKey = config.getApiKey('openai');
|
|
794
|
-
if (!apiKey)
|
|
795
|
-
throw new Error('OpenAI API key not configured');
|
|
796
|
-
const client = new OpenAI({ apiKey });
|
|
797
|
-
const responsesInput = toResponsesInput(messages);
|
|
798
|
-
const responsesTools = toResponsesTools(tools);
|
|
799
|
-
// Calculate dynamic max_tokens based on available context space
|
|
800
|
-
const dynamicMaxTokens = calculateMaxTokens('openai', model, messages, tools);
|
|
801
|
-
debugLog(`OpenAI Responses API request: model=${model}, max_tokens=${dynamicMaxTokens}`);
|
|
802
|
-
// Use streaming if callback provided
|
|
803
|
-
if (onToken) {
|
|
804
|
-
let content = '';
|
|
805
|
-
const toolCalls = [];
|
|
806
|
-
let finishReason = 'stop';
|
|
807
|
-
let inputTokens = 0;
|
|
808
|
-
let outputTokens = 0;
|
|
809
|
-
try {
|
|
810
|
-
// Note: OpenAI SDK types don't fully match Responses API yet
|
|
811
|
-
// We use our own type definitions and cast through unknown for SDK interop
|
|
812
|
-
const streamParams = {
|
|
813
|
-
model,
|
|
814
|
-
input: responsesInput,
|
|
815
|
-
tools: responsesTools.length > 0 ? responsesTools : undefined,
|
|
816
|
-
max_output_tokens: dynamicMaxTokens,
|
|
817
|
-
};
|
|
818
|
-
const stream = client.responses.stream(streamParams);
|
|
819
|
-
for await (const event of stream) {
|
|
820
|
-
const typedEvent = event;
|
|
821
|
-
if (isTextDeltaEvent(typedEvent)) {
|
|
822
|
-
content += typedEvent.delta;
|
|
823
|
-
onToken(typedEvent.delta);
|
|
824
|
-
}
|
|
825
|
-
else if (isFunctionCallDoneEvent(typedEvent)) {
|
|
826
|
-
toolCalls.push({
|
|
827
|
-
id: typedEvent.call_id || `call_${Date.now()}`,
|
|
828
|
-
name: typedEvent.name,
|
|
829
|
-
arguments: JSON.parse(typedEvent.arguments || '{}'),
|
|
830
|
-
});
|
|
831
|
-
finishReason = 'tool_use';
|
|
832
|
-
}
|
|
833
|
-
else if (isCompletedEvent(typedEvent)) {
|
|
834
|
-
const response = typedEvent.response;
|
|
835
|
-
if (response?.usage) {
|
|
836
|
-
inputTokens = response.usage.input_tokens || 0;
|
|
837
|
-
outputTokens = response.usage.output_tokens || 0;
|
|
838
|
-
}
|
|
839
|
-
if (response?.status === 'incomplete') {
|
|
840
|
-
finishReason = 'length';
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return {
|
|
845
|
-
content,
|
|
846
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
847
|
-
finishReason,
|
|
848
|
-
usage: { inputTokens, outputTokens },
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
catch (streamError) {
|
|
852
|
-
debugLog('Responses API streaming failed, falling back to non-streaming:', streamError);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Non-streaming request
|
|
856
|
-
const createParams = {
|
|
857
|
-
model,
|
|
858
|
-
input: responsesInput,
|
|
859
|
-
tools: responsesTools.length > 0 ? responsesTools : undefined,
|
|
860
|
-
max_output_tokens: dynamicMaxTokens,
|
|
861
|
-
};
|
|
862
|
-
const response = await client.responses.create(createParams);
|
|
863
|
-
// Extract content and tool calls from response
|
|
864
|
-
let content = response.output_text || '';
|
|
865
|
-
const toolCalls = [];
|
|
866
|
-
// Process output items for tool calls
|
|
867
|
-
for (const item of response.output) {
|
|
868
|
-
if (isFunctionCallOutput(item)) {
|
|
869
|
-
toolCalls.push({
|
|
870
|
-
id: item.call_id || item.id || `call_${Date.now()}`,
|
|
871
|
-
name: item.name,
|
|
872
|
-
arguments: typeof item.arguments === 'string'
|
|
873
|
-
? JSON.parse(item.arguments)
|
|
874
|
-
: item.arguments,
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
// Determine finish reason
|
|
879
|
-
let finishReason = 'stop';
|
|
880
|
-
if (toolCalls.length > 0) {
|
|
881
|
-
finishReason = 'tool_use';
|
|
882
|
-
}
|
|
883
|
-
else if (response.status === 'incomplete') {
|
|
884
|
-
finishReason = 'length';
|
|
885
|
-
}
|
|
886
|
-
return {
|
|
887
|
-
content,
|
|
888
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
889
|
-
finishReason,
|
|
890
|
-
usage: response.usage ? {
|
|
891
|
-
inputTokens: response.usage.input_tokens,
|
|
892
|
-
outputTokens: response.usage.output_tokens,
|
|
893
|
-
} : undefined,
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Chat with OpenAI
|
|
898
|
-
*/
|
|
899
|
-
async function chatOpenAI(messages, tools, model, onToken) {
|
|
900
|
-
// Route to Responses API for models that require it (o3, o4-mini, etc.)
|
|
901
|
-
if (requiresResponsesAPI(model)) {
|
|
902
|
-
return chatOpenAIResponses(messages, tools, model, onToken);
|
|
903
|
-
}
|
|
904
|
-
const apiKey = config.getApiKey('openai');
|
|
905
|
-
if (!apiKey)
|
|
906
|
-
throw new Error('OpenAI API key not configured');
|
|
907
|
-
const client = new OpenAI({ apiKey });
|
|
908
|
-
const openaiMessages = toOpenAIMessages(messages);
|
|
909
|
-
const openaiTools = toOpenAITools(tools);
|
|
910
|
-
// Calculate dynamic max_tokens based on available context space
|
|
911
|
-
const dynamicMaxTokens = calculateMaxTokens('openai', model, messages, tools);
|
|
912
|
-
debugLog(`OpenAI request: model=${model}, max_tokens=${dynamicMaxTokens}`);
|
|
913
|
-
// Use streaming if callback provided
|
|
914
|
-
// Stream text content while collecting tool calls
|
|
915
|
-
if (onToken) {
|
|
916
|
-
let content = '';
|
|
917
|
-
let toolCallDeltas = {};
|
|
918
|
-
let finishReason = 'stop';
|
|
919
|
-
try {
|
|
920
|
-
const stream = await client.chat.completions.create({
|
|
921
|
-
model,
|
|
922
|
-
messages: openaiMessages,
|
|
923
|
-
tools: openaiTools.length > 0 ? openaiTools : undefined,
|
|
924
|
-
max_tokens: dynamicMaxTokens,
|
|
925
|
-
stream: true,
|
|
926
|
-
});
|
|
927
|
-
for await (const chunk of stream) {
|
|
928
|
-
const choice = chunk.choices[0];
|
|
929
|
-
if (!choice)
|
|
930
|
-
continue;
|
|
931
|
-
// Handle text content
|
|
932
|
-
const textDelta = choice.delta?.content;
|
|
933
|
-
if (textDelta) {
|
|
934
|
-
content += textDelta;
|
|
935
|
-
onToken(textDelta);
|
|
936
|
-
}
|
|
937
|
-
// Handle tool calls (collect deltas)
|
|
938
|
-
const toolCallDelta = choice.delta?.tool_calls;
|
|
939
|
-
if (toolCallDelta) {
|
|
940
|
-
for (const tc of toolCallDelta) {
|
|
941
|
-
if (!toolCallDeltas[tc.index]) {
|
|
942
|
-
toolCallDeltas[tc.index] = { id: '', name: '', arguments: '' };
|
|
943
|
-
}
|
|
944
|
-
if (tc.id)
|
|
945
|
-
toolCallDeltas[tc.index].id = tc.id;
|
|
946
|
-
if (tc.function?.name)
|
|
947
|
-
toolCallDeltas[tc.index].name = tc.function.name;
|
|
948
|
-
if (tc.function?.arguments)
|
|
949
|
-
toolCallDeltas[tc.index].arguments += tc.function.arguments;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
// Track finish reason
|
|
953
|
-
if (choice.finish_reason === 'tool_calls') {
|
|
954
|
-
finishReason = 'tool_use';
|
|
955
|
-
}
|
|
956
|
-
else if (choice.finish_reason === 'length') {
|
|
957
|
-
finishReason = 'length';
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
// Convert tool call deltas to tool calls
|
|
961
|
-
const toolCalls = Object.values(toolCallDeltas)
|
|
962
|
-
.filter(tc => tc.id && tc.name)
|
|
963
|
-
.map(tc => ({
|
|
964
|
-
id: tc.id,
|
|
965
|
-
name: tc.name,
|
|
966
|
-
arguments: JSON.parse(tc.arguments || '{}'),
|
|
967
|
-
}));
|
|
968
|
-
if (toolCalls.length > 0) {
|
|
969
|
-
finishReason = 'tool_use';
|
|
970
|
-
}
|
|
971
|
-
return {
|
|
972
|
-
content,
|
|
973
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
974
|
-
finishReason,
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
catch (streamError) {
|
|
978
|
-
// Fall back to non-streaming on error
|
|
979
|
-
console.error('Streaming failed, falling back to non-streaming:', streamError);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
// Non-streaming request
|
|
983
|
-
const response = await client.chat.completions.create({
|
|
984
|
-
model,
|
|
985
|
-
messages: openaiMessages,
|
|
986
|
-
tools: openaiTools.length > 0 ? openaiTools : undefined,
|
|
987
|
-
max_tokens: dynamicMaxTokens,
|
|
988
|
-
});
|
|
989
|
-
if (!response.choices || response.choices.length === 0) {
|
|
990
|
-
throw new Error('Empty response from OpenAI API');
|
|
991
|
-
}
|
|
992
|
-
const choice = response.choices[0];
|
|
993
|
-
const message = choice.message;
|
|
994
|
-
const toolCalls = parseOpenAIToolCalls(message.tool_calls);
|
|
995
|
-
// Map OpenAI finish reasons
|
|
996
|
-
let finishReason = 'stop';
|
|
997
|
-
if (choice.finish_reason === 'tool_calls') {
|
|
998
|
-
finishReason = 'tool_use';
|
|
999
|
-
}
|
|
1000
|
-
else if (choice.finish_reason === 'length') {
|
|
1001
|
-
finishReason = 'length';
|
|
1002
|
-
}
|
|
1003
|
-
return {
|
|
1004
|
-
content: message.content || '',
|
|
1005
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1006
|
-
finishReason,
|
|
1007
|
-
usage: response.usage ? {
|
|
1008
|
-
inputTokens: response.usage.prompt_tokens,
|
|
1009
|
-
outputTokens: response.usage.completion_tokens,
|
|
1010
|
-
} : undefined,
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Chat with OpenAI-compatible APIs (OpenRouter, Together, Groq, Mistral, etc.)
|
|
1015
|
-
*/
|
|
1016
|
-
async function chatOpenAICompatible(provider, messages, tools, model, onToken) {
|
|
1017
|
-
// Ollama and LiteLLM use base URL, others use API key
|
|
1018
|
-
let apiKey;
|
|
1019
|
-
let baseURL;
|
|
1020
|
-
if (provider === 'ollama') {
|
|
1021
|
-
const ollamaBase = config.getBaseUrl('ollama') || 'http://localhost:11434';
|
|
1022
|
-
// Append /v1 for OpenAI-compatible endpoint, unless already present
|
|
1023
|
-
baseURL = ollamaBase.endsWith('/v1') ? ollamaBase : `${ollamaBase}/v1`;
|
|
1024
|
-
apiKey = 'ollama'; // Ollama doesn't require a real API key
|
|
1025
|
-
}
|
|
1026
|
-
else if (provider === 'litellm') {
|
|
1027
|
-
const litellmBase = config.getBaseUrl('litellm') || 'http://localhost:4000';
|
|
1028
|
-
// Append /v1 for OpenAI-compatible endpoint, unless already present
|
|
1029
|
-
baseURL = litellmBase.endsWith('/v1') ? litellmBase : `${litellmBase}/v1`;
|
|
1030
|
-
apiKey = config.getApiKey('litellm') || 'litellm'; // LiteLLM may or may not require key
|
|
1031
|
-
}
|
|
1032
|
-
else {
|
|
1033
|
-
apiKey = config.getApiKey(provider);
|
|
1034
|
-
if (!apiKey)
|
|
1035
|
-
throw new Error(`${provider} API key not configured`);
|
|
1036
|
-
baseURL = PROVIDER_BASE_URLS[provider];
|
|
1037
|
-
if (!baseURL)
|
|
1038
|
-
throw new Error(`Unknown provider: ${provider}`);
|
|
1039
|
-
}
|
|
1040
|
-
const client = new OpenAI({ apiKey, baseURL });
|
|
1041
|
-
const openaiMessages = toOpenAIMessages(messages);
|
|
1042
|
-
const openaiTools = toOpenAITools(tools);
|
|
1043
|
-
// Calculate dynamic max_tokens based on available context space
|
|
1044
|
-
const dynamicMaxTokens = calculateMaxTokens(provider, model, messages, tools);
|
|
1045
|
-
debugLog(`${provider} request: model=${model}, max_tokens=${dynamicMaxTokens}`);
|
|
1046
|
-
// Use streaming if callback provided
|
|
1047
|
-
// Stream text content while collecting tool calls
|
|
1048
|
-
if (onToken) {
|
|
1049
|
-
let content = '';
|
|
1050
|
-
let toolCallDeltas = {};
|
|
1051
|
-
let finishReason = 'stop';
|
|
1052
|
-
try {
|
|
1053
|
-
const stream = await client.chat.completions.create({
|
|
1054
|
-
model,
|
|
1055
|
-
messages: openaiMessages,
|
|
1056
|
-
tools: openaiTools.length > 0 ? openaiTools : undefined,
|
|
1057
|
-
max_tokens: dynamicMaxTokens,
|
|
1058
|
-
stream: true,
|
|
1059
|
-
});
|
|
1060
|
-
for await (const chunk of stream) {
|
|
1061
|
-
const choice = chunk.choices[0];
|
|
1062
|
-
if (!choice)
|
|
1063
|
-
continue;
|
|
1064
|
-
// Handle text content
|
|
1065
|
-
const textDelta = choice.delta?.content;
|
|
1066
|
-
if (textDelta) {
|
|
1067
|
-
content += textDelta;
|
|
1068
|
-
onToken(textDelta);
|
|
1069
|
-
}
|
|
1070
|
-
// Handle tool calls (collect deltas)
|
|
1071
|
-
const toolCallDelta = choice.delta?.tool_calls;
|
|
1072
|
-
if (toolCallDelta) {
|
|
1073
|
-
for (const tc of toolCallDelta) {
|
|
1074
|
-
if (!toolCallDeltas[tc.index]) {
|
|
1075
|
-
toolCallDeltas[tc.index] = { id: '', name: '', arguments: '' };
|
|
1076
|
-
}
|
|
1077
|
-
if (tc.id)
|
|
1078
|
-
toolCallDeltas[tc.index].id = tc.id;
|
|
1079
|
-
if (tc.function?.name)
|
|
1080
|
-
toolCallDeltas[tc.index].name = tc.function.name;
|
|
1081
|
-
if (tc.function?.arguments)
|
|
1082
|
-
toolCallDeltas[tc.index].arguments += tc.function.arguments;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
// Track finish reason
|
|
1086
|
-
if (choice.finish_reason === 'tool_calls') {
|
|
1087
|
-
finishReason = 'tool_use';
|
|
1088
|
-
}
|
|
1089
|
-
else if (choice.finish_reason === 'length') {
|
|
1090
|
-
finishReason = 'length';
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
// Convert tool call deltas to tool calls
|
|
1094
|
-
const toolCalls = Object.values(toolCallDeltas)
|
|
1095
|
-
.filter(tc => tc.id && tc.name)
|
|
1096
|
-
.map(tc => ({
|
|
1097
|
-
id: tc.id,
|
|
1098
|
-
name: tc.name,
|
|
1099
|
-
arguments: JSON.parse(tc.arguments || '{}'),
|
|
1100
|
-
}));
|
|
1101
|
-
if (toolCalls.length > 0) {
|
|
1102
|
-
finishReason = 'tool_use';
|
|
1103
|
-
}
|
|
1104
|
-
return {
|
|
1105
|
-
content,
|
|
1106
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1107
|
-
finishReason,
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
catch (streamError) {
|
|
1111
|
-
// Fall back to non-streaming on error
|
|
1112
|
-
console.error('Streaming failed, falling back to non-streaming:', streamError);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
// Non-streaming request
|
|
1116
|
-
const response = await client.chat.completions.create({
|
|
1117
|
-
model,
|
|
1118
|
-
messages: openaiMessages,
|
|
1119
|
-
tools: openaiTools.length > 0 ? openaiTools : undefined,
|
|
1120
|
-
max_tokens: dynamicMaxTokens,
|
|
1121
|
-
});
|
|
1122
|
-
if (!response.choices || response.choices.length === 0) {
|
|
1123
|
-
throw new Error(`Empty response from ${provider} API`);
|
|
1124
|
-
}
|
|
1125
|
-
const choice = response.choices[0];
|
|
1126
|
-
const message = choice.message;
|
|
1127
|
-
const toolCalls = parseOpenAIToolCalls(message.tool_calls);
|
|
1128
|
-
// Map finish reasons
|
|
1129
|
-
let finishReason = 'stop';
|
|
1130
|
-
if (choice.finish_reason === 'tool_calls') {
|
|
1131
|
-
finishReason = 'tool_use';
|
|
1132
|
-
}
|
|
1133
|
-
else if (choice.finish_reason === 'length') {
|
|
1134
|
-
finishReason = 'length';
|
|
1135
|
-
}
|
|
1136
|
-
return {
|
|
1137
|
-
content: message.content || '',
|
|
1138
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1139
|
-
finishReason,
|
|
1140
|
-
usage: response.usage ? {
|
|
1141
|
-
inputTokens: response.usage.prompt_tokens,
|
|
1142
|
-
outputTokens: response.usage.completion_tokens,
|
|
1143
|
-
} : undefined,
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
//# sourceMappingURL=providers.js.map
|