@xelauvas/xela-cli 0.3.0 → 0.4.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/local/README.md +112 -0
- package/local/start-qwen.sh +47 -0
- package/local/start-tunnel.sh +36 -0
- package/local/xela-local.sh +139 -0
- package/package.json +19 -5
- package/run.sh +33 -0
- package/src/Tool.ts +1 -1
- package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
- package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
- package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
- package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
- package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
- package/src/_shims/bun_bundle.js +79 -4
- package/src/_shims/cjs_stub.cjs +32 -10
- package/src/_shims/loader.js +104 -5
- package/src/_shims/openai_adapter.ts +446 -230
- package/src/_shims/register.js +16 -3
- package/src/assistant/gate.ts +15 -0
- package/src/assistant/index.ts +89 -0
- package/src/bootstrap/state.ts +6 -1
- package/src/bridge/peerSessions.ts +18 -0
- package/src/cli/print.ts +5 -0
- package/src/commands/assistant/assistant.ts +17 -0
- package/src/commands/assistant/index.ts +11 -0
- package/src/commands/brief.ts +3 -1
- package/src/commands/buddy/buddy.ts +88 -0
- package/src/commands/buddy/index.ts +12 -0
- package/src/commands/model/model.tsx +8 -23
- package/src/commands/peers/index.ts +11 -0
- package/src/commands/peers/peers.ts +24 -0
- package/src/commands/proactive/proactive.ts +64 -0
- package/src/commands/proactive.ts +12 -0
- package/src/commands/workflows/index.ts +12 -0
- package/src/commands/workflows/workflows.ts +36 -0
- package/src/commands.ts +4 -8
- package/src/components/Messages.tsx +1 -1
- package/src/components/ModelPicker.tsx +4 -34
- package/src/components/Spinner.tsx +54 -1
- package/src/components/StructuredDiff/colorDiff.ts +3 -1
- package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
- package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
- package/src/components/permissions/PermissionRequest.tsx +4 -2
- package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
- package/src/constants/prompts.ts +26 -0
- package/src/ink/terminal.ts +9 -1
- package/src/jobs/classifier.ts +103 -0
- package/src/memdir/paths.ts +13 -1
- package/src/proactive/index.ts +158 -0
- package/src/proactive/useProactive.ts +78 -0
- package/src/screens/REPL.tsx +28 -8
- package/src/services/api/claude.ts +6 -1
- package/src/services/compact/autoCompact.ts +18 -4
- package/src/services/compact/cachedMCConfig.ts +1 -0
- package/src/services/compact/cachedMicrocompact.ts +113 -0
- package/src/services/compact/compact.ts +22 -2
- package/src/services/compact/reactiveCompact.ts +251 -0
- package/src/services/compact/snipCompact.ts +282 -0
- package/src/services/compact/snipProjection.ts +50 -0
- package/src/services/contextCollapse/index.ts +322 -0
- package/src/services/contextCollapse/operations.ts +187 -0
- package/src/services/contextCollapse/persist.ts +40 -0
- package/src/services/extractMemories/extractMemories.ts +11 -2
- package/src/services/skillSearch/featureCheck.ts +12 -0
- package/src/services/skillSearch/localSearch.ts +98 -0
- package/src/services/skillSearch/prefetch.ts +125 -0
- package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
- package/src/services/skillSearch/remoteSkillState.ts +21 -0
- package/src/services/skillSearch/signals.ts +7 -0
- package/src/services/skillSearch/telemetry.ts +9 -0
- package/src/setup.ts +10 -5
- package/src/skills/bundled/dream.ts +31 -0
- package/src/skills/bundled/hunter.ts +45 -0
- package/src/skills/bundled/index.ts +10 -23
- package/src/skills/bundled/runSkillGenerator.ts +61 -0
- package/src/skills/mcpSkills.ts +101 -0
- package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
- package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
- package/src/tasks.ts +22 -8
- package/src/tools/AgentTool/runAgent.ts +11 -0
- package/src/tools/BriefTool/BriefTool.ts +2 -0
- package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
- package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
- package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
- package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
- package/src/tools/MCPTool/MCPTool.ts +6 -0
- package/src/tools/MonitorTool/MonitorTool.ts +273 -0
- package/src/tools/MonitorTool/UI.tsx +25 -0
- package/src/tools/MonitorTool/events.ts +84 -0
- package/src/tools/MonitorTool/prompt.ts +45 -0
- package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
- package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
- package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
- package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
- package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
- package/src/tools/SendUserFileTool/prompt.ts +7 -0
- package/src/tools/SleepTool/SleepTool.ts +126 -0
- package/src/tools/SnipTool/SnipTool.ts +90 -0
- package/src/tools/SnipTool/prompt.ts +5 -0
- package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
- package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
- package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
- package/src/tools/ToolSearchTool/prompt.ts +1 -1
- package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
- package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
- package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
- package/src/tools/WebBrowserTool/prompt.ts +16 -0
- package/src/tools/WorkflowTool/UI.tsx +27 -0
- package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
- package/src/tools/WorkflowTool/bundled/index.ts +88 -0
- package/src/tools/WorkflowTool/constants.ts +7 -0
- package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
- package/src/tools/WorkflowTool/prompt.ts +38 -0
- package/src/tools/WorkflowTool/runtime.ts +331 -0
- package/src/tools.ts +36 -31
- package/src/utils/betas.ts +5 -0
- package/src/utils/context.ts +9 -0
- package/src/utils/conversationRecovery.ts +1 -1
- package/src/utils/forkedAgent.ts +4 -0
- package/src/utils/logoV2Utils.ts +7 -3
- package/src/utils/messageQueueManager.ts +23 -9
- package/src/utils/model/model.ts +10 -9
- package/src/utils/model/modelOptions.ts +0 -21
- package/src/utils/model/validateModel.ts +0 -19
- package/src/utils/permissions/permissionSetup.ts +5 -0
- package/src/utils/permissions/permissions.ts +25 -0
- package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
- package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
- package/src/utils/permissions/yoloClassifier.ts +28 -6
- package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
- package/src/utils/systemThemeWatcher.ts +62 -0
- package/src/utils/udsClient.ts +115 -0
- package/src/utils/udsMessaging.ts +157 -0
- package/src/utils/yaml.ts +251 -5
- package/src/utils/model/providerModels.ts +0 -168
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* Multi-Provider Adapter - Makes any OpenAI-compatible API look like Anthropic SDK.
|
|
3
3
|
*
|
|
4
4
|
* Supports: OpenAI, Groq, OpenRouter, Moonshot/Kimi, DeepSeek, Ollama, Mistral,
|
|
5
|
-
* Together AI, Fireworks, and any OpenAI-compatible endpoint.
|
|
5
|
+
* Together AI, Fireworks, llama.cpp and any OpenAI-compatible endpoint.
|
|
6
6
|
*
|
|
7
7
|
* Environment variables:
|
|
8
|
-
* OPENAI_API_KEY
|
|
9
|
-
* OPENAI_BASE_URL
|
|
10
|
-
* OPENAI_MODEL
|
|
8
|
+
* OPENAI_API_KEY - API key (required)
|
|
9
|
+
* OPENAI_BASE_URL - Custom base URL (optional, auto-detected from key prefix)
|
|
10
|
+
* OPENAI_MODEL - Model name (optional, auto-detected from provider)
|
|
11
|
+
* OPENAI_MAX_TOKENS - Cap on output tokens per request (optional, per-provider default)
|
|
12
|
+
* OPENAI_TEMPERATURE - Force a sampling temperature (optional; local servers keep their own default)
|
|
13
|
+
* OPENAI_THINKING - "on" | "off" to force reasoning for local servers (optional; follows the app otherwise)
|
|
14
|
+
* OPENAI_WEB_SEARCH - "off" to disable the DuckDuckGo fallback for the WebSearch tool (optional)
|
|
15
|
+
* OPENAI_DEBUG_LOG - Path of a JSONL file that receives every request/response summary (optional)
|
|
11
16
|
*
|
|
12
17
|
* Quick start examples:
|
|
13
18
|
* # OpenAI (default)
|
|
@@ -19,23 +24,14 @@
|
|
|
19
24
|
* # OpenRouter (many models, some free)
|
|
20
25
|
* OPENAI_API_KEY=sk-or-... OPENAI_MODEL=google/gemini-2.0-flash-001
|
|
21
26
|
*
|
|
22
|
-
* # Moonshot/Kimi (cheap)
|
|
23
|
-
* OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.moonshot.ai/v1 OPENAI_MODEL=moonshot-v1-32k
|
|
24
|
-
*
|
|
25
27
|
* # DeepSeek (very cheap)
|
|
26
28
|
* OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com OPENAI_MODEL=deepseek-chat
|
|
27
29
|
*
|
|
28
|
-
* # Ollama (local, free)
|
|
29
|
-
* OPENAI_API_KEY=
|
|
30
|
-
*
|
|
31
|
-
* # Mistral
|
|
32
|
-
* OPENAI_API_KEY=... OPENAI_BASE_URL=https://api.mistral.ai/v1 OPENAI_MODEL=mistral-large-latest
|
|
33
|
-
*
|
|
34
|
-
* # Together AI
|
|
35
|
-
* OPENAI_API_KEY=... OPENAI_BASE_URL=https://api.together.xyz/v1 OPENAI_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo
|
|
30
|
+
* # llama.cpp / Ollama / LM Studio (local, free)
|
|
31
|
+
* OPENAI_API_KEY=local OPENAI_BASE_URL=http://localhost:8080/v1 OPENAI_MODEL=qwen3.6-27b
|
|
36
32
|
*/
|
|
37
33
|
import OpenAI from 'openai'
|
|
38
|
-
import {
|
|
34
|
+
import { appendFileSync } from 'fs'
|
|
39
35
|
|
|
40
36
|
// Auto-detect provider from API key prefix or base URL
|
|
41
37
|
function detectProvider(): { baseURL?: string; model: string; maxTokens: number } {
|
|
@@ -49,7 +45,8 @@ function detectProvider(): { baseURL?: string; model: string; maxTokens: number
|
|
|
49
45
|
if (baseURL.includes('openrouter.ai')) return { baseURL, model: model || 'google/gemini-2.0-flash-001', maxTokens: 8192 }
|
|
50
46
|
if (baseURL.includes('moonshot.cn') || baseURL.includes('moonshot.ai')) return { baseURL, model: model || 'moonshot-v1-32k', maxTokens: 4096 }
|
|
51
47
|
if (baseURL.includes('deepseek.com')) return { baseURL, model: model || 'deepseek-chat', maxTokens: 8192 }
|
|
52
|
-
|
|
48
|
+
// Local servers (llama.cpp, Ollama, LM Studio): reasoning models spend thousands of tokens thinking before answering
|
|
49
|
+
if (isLocalUrl(baseURL)) return { baseURL, model: model || 'llama3.2', maxTokens: 32768 }
|
|
53
50
|
if (baseURL.includes('mistral.ai')) return { baseURL, model: model || 'mistral-large-latest', maxTokens: 8192 }
|
|
54
51
|
if (baseURL.includes('together.xyz')) return { baseURL, model: model || 'meta-llama/Llama-3.3-70B-Instruct-Turbo', maxTokens: 8192 }
|
|
55
52
|
if (baseURL.includes('fireworks.ai')) return { baseURL, model: model || 'accounts/fireworks/models/llama-v3p3-70b-instruct', maxTokens: 8192 }
|
|
@@ -64,20 +61,57 @@ function detectProvider(): { baseURL?: string; model: string; maxTokens: number
|
|
|
64
61
|
return { model: model || 'gpt-4o', maxTokens: 16384 }
|
|
65
62
|
}
|
|
66
63
|
|
|
64
|
+
function isLocalUrl(url: string): boolean {
|
|
65
|
+
return /localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0/.test(url)
|
|
66
|
+
}
|
|
67
|
+
|
|
67
68
|
const PROVIDER = detectProvider()
|
|
68
69
|
const MODEL = PROVIDER.model
|
|
69
|
-
const
|
|
70
|
+
const IS_LOCAL = !!PROVIDER.baseURL && isLocalUrl(PROVIDER.baseURL)
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
function envNumber(name: string): number | undefined {
|
|
73
|
+
const n = Number(process.env[name])
|
|
74
|
+
return process.env[name] && Number.isFinite(n) && n > 0 ? n : undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// OPENAI_MAX_TOKENS overrides the per-provider output cap
|
|
78
|
+
const MAX_TOKENS = envNumber('OPENAI_MAX_TOKENS') ?? PROVIDER.maxTokens
|
|
79
|
+
|
|
80
|
+
// OPENAI_TEMPERATURE forces a temperature. Local servers otherwise keep their own
|
|
81
|
+
// sampling defaults (tuned per model), instead of the Claude-oriented value the app sends.
|
|
82
|
+
function resolveTemperature(requested: number | undefined): number | undefined {
|
|
83
|
+
const forced = process.env.OPENAI_TEMPERATURE
|
|
84
|
+
if (forced !== undefined && forced !== '' && Number.isFinite(Number(forced))) return Number(forced)
|
|
85
|
+
return IS_LOCAL ? undefined : requested
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Reasoning on/off for local servers. Follows the app's thinking config (and the
|
|
89
|
+
// effort level) unless OPENAI_THINKING forces it. Sent as chat_template_kwargs,
|
|
90
|
+
// which llama.cpp understands; hosted providers would reject the unknown field.
|
|
91
|
+
function resolveThinking(params: any): boolean | undefined {
|
|
92
|
+
if (!IS_LOCAL) return undefined
|
|
93
|
+
const forced = (process.env.OPENAI_THINKING || '').toLowerCase()
|
|
94
|
+
if (forced === 'on' || forced === 'true' || forced === '1') return true
|
|
95
|
+
if (forced === 'off' || forced === 'false' || forced === '0') return false
|
|
96
|
+
if (!params.thinking || params.thinking.type === 'disabled') return false
|
|
97
|
+
if (params.output_config?.effort === 'low') return false
|
|
98
|
+
// Conversation compaction is a summarization job: reasoning adds minutes on a
|
|
99
|
+
// local model and nothing to the summary
|
|
100
|
+
if (isCompactionRequest(params)) return false
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isCompactionRequest(params: any): boolean {
|
|
105
|
+
const last = [...(params.messages || [])].reverse().find((m: any) => m.role === 'user')
|
|
106
|
+
return !!last && /Your task is to create a detailed summary/.test(blockText(last.content))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function debugLog(entry: Record<string, unknown>): void {
|
|
110
|
+
const path = process.env.OPENAI_DEBUG_LOG
|
|
111
|
+
if (!path) return
|
|
112
|
+
try {
|
|
113
|
+
appendFileSync(path, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n')
|
|
114
|
+
} catch {}
|
|
81
115
|
}
|
|
82
116
|
|
|
83
117
|
let _openaiClient: OpenAI | null = null
|
|
@@ -96,6 +130,12 @@ function getOpenAI(): OpenAI {
|
|
|
96
130
|
return _openaiClient
|
|
97
131
|
}
|
|
98
132
|
|
|
133
|
+
function blockText(content: any): string {
|
|
134
|
+
if (typeof content === 'string') return content
|
|
135
|
+
if (Array.isArray(content)) return content.map((c: any) => (c?.type === 'text' ? c.text : c?.text || '')).join('\n')
|
|
136
|
+
return content == null ? '' : JSON.stringify(content)
|
|
137
|
+
}
|
|
138
|
+
|
|
99
139
|
// Convert Anthropic-style messages to OpenAI format
|
|
100
140
|
function convertMessages(messages: any[]): any[] {
|
|
101
141
|
const result: any[] = []
|
|
@@ -111,17 +151,12 @@ function convertMessages(messages: any[]): any[] {
|
|
|
111
151
|
parts.push({ type: 'text', text: block.text })
|
|
112
152
|
} else if (block.type === 'tool_result') {
|
|
113
153
|
// Tool results in Anthropic are content blocks, in OpenAI they're separate messages
|
|
114
|
-
|
|
115
|
-
? block.content
|
|
116
|
-
: Array.isArray(block.content)
|
|
117
|
-
? block.content.map((c: any) => c.text || '').join('\n')
|
|
118
|
-
: JSON.stringify(block.content || '')
|
|
154
|
+
// (they must directly follow the assistant tool_calls message, before any user text)
|
|
119
155
|
result.push({
|
|
120
156
|
role: 'tool',
|
|
121
157
|
tool_call_id: block.tool_use_id,
|
|
122
|
-
content,
|
|
158
|
+
content: blockText(block.content),
|
|
123
159
|
})
|
|
124
|
-
continue
|
|
125
160
|
} else if (block.type === 'image') {
|
|
126
161
|
parts.push({
|
|
127
162
|
type: 'image_url',
|
|
@@ -138,10 +173,13 @@ function convertMessages(messages: any[]): any[] {
|
|
|
138
173
|
result.push({ role: 'assistant', content: msg.content })
|
|
139
174
|
} else if (Array.isArray(msg.content)) {
|
|
140
175
|
let text = ''
|
|
176
|
+
let reasoning = ''
|
|
141
177
|
const toolCalls: any[] = []
|
|
142
178
|
for (const block of msg.content) {
|
|
143
179
|
if (block.type === 'text') {
|
|
144
180
|
text += block.text
|
|
181
|
+
} else if (block.type === 'thinking') {
|
|
182
|
+
reasoning += block.thinking || ''
|
|
145
183
|
} else if (block.type === 'tool_use') {
|
|
146
184
|
toolCalls.push({
|
|
147
185
|
id: block.id,
|
|
@@ -152,11 +190,15 @@ function convertMessages(messages: any[]): any[] {
|
|
|
152
190
|
},
|
|
153
191
|
})
|
|
154
192
|
}
|
|
155
|
-
//
|
|
193
|
+
// redacted_thinking is dropped
|
|
156
194
|
}
|
|
157
195
|
const assistantMsg: any = { role: 'assistant' }
|
|
158
|
-
|
|
196
|
+
// A turn with only thinking still needs content, or strict servers reject the message
|
|
197
|
+
if (text || toolCalls.length === 0) assistantMsg.content = text
|
|
159
198
|
if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls
|
|
199
|
+
// Reasoning models (Qwen3, DeepSeek) do better in multi-step tool loops when they
|
|
200
|
+
// see their own earlier reasoning; llama.cpp passes reasoning_content to the template
|
|
201
|
+
if (IS_LOCAL && reasoning) assistantMsg.reasoning_content = reasoning
|
|
160
202
|
result.push(assistantMsg)
|
|
161
203
|
}
|
|
162
204
|
}
|
|
@@ -165,11 +207,17 @@ function convertMessages(messages: any[]): any[] {
|
|
|
165
207
|
return result
|
|
166
208
|
}
|
|
167
209
|
|
|
210
|
+
// Anthropic server-side tools (web_search_20250305, computer_20241022, ...) run inside
|
|
211
|
+
// Anthropic's API. They have no input schema and nothing on the other end here.
|
|
212
|
+
function isServerTool(tool: any): boolean {
|
|
213
|
+
return typeof tool?.type === 'string' && tool.type !== 'custom' && !tool.input_schema
|
|
214
|
+
}
|
|
215
|
+
|
|
168
216
|
// Convert Anthropic tool schemas to OpenAI function format
|
|
169
217
|
function convertTools(tools: any[]): any[] {
|
|
170
218
|
if (!tools || tools.length === 0) return []
|
|
171
219
|
|
|
172
|
-
return tools.map((tool: any) => {
|
|
220
|
+
return tools.filter((t: any) => !isServerTool(t)).map((tool: any) => {
|
|
173
221
|
// Handle different Anthropic tool formats
|
|
174
222
|
const name = tool.name || tool.function?.name
|
|
175
223
|
const description = tool.description || tool.function?.description || ''
|
|
@@ -186,6 +234,17 @@ function convertTools(tools: any[]): any[] {
|
|
|
186
234
|
}).filter((t: any) => t.function.name)
|
|
187
235
|
}
|
|
188
236
|
|
|
237
|
+
function convertToolChoice(choice: any): any {
|
|
238
|
+
if (!choice) return undefined
|
|
239
|
+
switch (choice.type) {
|
|
240
|
+
case 'auto': return 'auto'
|
|
241
|
+
case 'any': return 'required'
|
|
242
|
+
case 'none': return 'none'
|
|
243
|
+
case 'tool': return { type: 'function', function: { name: choice.name } }
|
|
244
|
+
default: return undefined
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
189
248
|
// Map OpenAI finish reason to Anthropic stop reason
|
|
190
249
|
function mapStopReason(finishReason: string | null): string {
|
|
191
250
|
switch (finishReason) {
|
|
@@ -197,15 +256,28 @@ function mapStopReason(finishReason: string | null): string {
|
|
|
197
256
|
}
|
|
198
257
|
}
|
|
199
258
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
function emptyUsage(): any {
|
|
260
|
+
return {
|
|
261
|
+
input_tokens: 0,
|
|
262
|
+
output_tokens: 0,
|
|
263
|
+
cache_creation_input_tokens: 0,
|
|
264
|
+
cache_read_input_tokens: 0,
|
|
265
|
+
server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },
|
|
266
|
+
service_tier: 'standard',
|
|
267
|
+
cache_creation: {
|
|
268
|
+
ephemeral_1h_input_tokens: 0,
|
|
269
|
+
ephemeral_5m_input_tokens: 0,
|
|
270
|
+
},
|
|
271
|
+
inference_geo: '',
|
|
272
|
+
iterations: [],
|
|
273
|
+
speed: 'standard',
|
|
274
|
+
}
|
|
275
|
+
}
|
|
205
276
|
|
|
277
|
+
// Build the OpenAI request body from Anthropic-style params
|
|
278
|
+
function buildRequest(params: any, stream: boolean): any {
|
|
206
279
|
const openaiMessages: any[] = []
|
|
207
280
|
|
|
208
|
-
// Add system prompt
|
|
209
281
|
if (params.system) {
|
|
210
282
|
const systemText = typeof params.system === 'string'
|
|
211
283
|
? params.system
|
|
@@ -217,168 +289,333 @@ async function* createAnthropicStyleStream(params: any, signal?: AbortSignal): A
|
|
|
217
289
|
}
|
|
218
290
|
}
|
|
219
291
|
|
|
220
|
-
// Convert messages
|
|
221
292
|
openaiMessages.push(...convertMessages(params.messages || []))
|
|
222
293
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const requestParams: any = {
|
|
227
|
-
model,
|
|
294
|
+
const request: any = {
|
|
295
|
+
// Always use the configured MODEL — internal Claude model names don't exist on OpenAI-compatible providers
|
|
296
|
+
model: MODEL,
|
|
228
297
|
messages: openaiMessages,
|
|
229
|
-
stream: true,
|
|
230
298
|
max_completion_tokens: Math.min(params.max_tokens || 8192, MAX_TOKENS),
|
|
231
299
|
}
|
|
300
|
+
if (stream) {
|
|
301
|
+
request.stream = true
|
|
302
|
+
// Ask for a final usage chunk so context tracking and auto-compact see real token counts
|
|
303
|
+
request.stream_options = { include_usage: true }
|
|
304
|
+
}
|
|
232
305
|
|
|
306
|
+
const tools = convertTools(params.tools || [])
|
|
233
307
|
if (tools.length > 0) {
|
|
234
|
-
|
|
308
|
+
request.tools = tools
|
|
309
|
+
const toolChoice = convertToolChoice(params.tool_choice)
|
|
310
|
+
if (toolChoice !== undefined) request.tool_choice = toolChoice
|
|
235
311
|
}
|
|
236
312
|
|
|
237
|
-
|
|
238
|
-
|
|
313
|
+
const temperature = resolveTemperature(params.temperature)
|
|
314
|
+
if (temperature !== undefined) request.temperature = temperature
|
|
315
|
+
|
|
316
|
+
const thinking = resolveThinking(params)
|
|
317
|
+
if (thinking !== undefined) request.chat_template_kwargs = { enable_thinking: thinking }
|
|
318
|
+
|
|
319
|
+
return request
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function summarizeRequest(request: any): Record<string, unknown> {
|
|
323
|
+
const messages: any[] = request.messages || []
|
|
324
|
+
// OPENAI_DEBUG_FULL=1 logs the complete message list (large)
|
|
325
|
+
if (process.env.OPENAI_DEBUG_FULL) {
|
|
326
|
+
return request
|
|
239
327
|
}
|
|
328
|
+
return {
|
|
329
|
+
...request,
|
|
330
|
+
messages: `${messages.length} messages`,
|
|
331
|
+
reasoning_msgs: messages.filter(m => m.reasoning_content).length,
|
|
332
|
+
tools: (request.tools || []).map((t: any) => t.function.name),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Re-throw provider errors as Anthropic APIError so the app's retry and
|
|
337
|
+
// compaction logic recognizes them
|
|
338
|
+
async function toAnthropicError(err: any): Promise<Error> {
|
|
339
|
+
const { APIError } = await import('@anthropic-ai/sdk')
|
|
340
|
+
const status = err?.status || 500
|
|
341
|
+
let message: string = err?.message || 'OpenAI API error'
|
|
342
|
+
// llama.cpp: "request (70123 tokens) exceeds the available context size (65536 tokens)"
|
|
343
|
+
// The app triggers auto-compact on Anthropic's wording, so translate it.
|
|
344
|
+
const ctx = /\((\d+) tokens\) exceeds the available context size \((\d+)/i.exec(message)
|
|
345
|
+
|| /(\d+) tokens? .*?exceed.*?(\d+)/i.exec(message)
|
|
346
|
+
if (ctx) {
|
|
347
|
+
message = `prompt is too long: ${ctx[1]} tokens > ${ctx[2]} maximum`
|
|
348
|
+
}
|
|
349
|
+
debugLog({ error: message, status })
|
|
350
|
+
// APIError builds its .message from the body's `message` when present, so
|
|
351
|
+
// the translated text must go into the body too, or the raw provider text
|
|
352
|
+
// wins and prompt-too-long detection misses it.
|
|
353
|
+
const body = { ...(typeof err?.error === 'object' && err.error ? err.error : {}), message }
|
|
354
|
+
return new APIError(status, body, message, new Headers())
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// WebSearch fallback. The app's WebSearch tool asks the model to run Anthropic's
|
|
359
|
+
// server-side web_search tool and parses the resulting content blocks. No other
|
|
360
|
+
// provider has that tool, so the search runs here (DuckDuckGo Lite, no API key)
|
|
361
|
+
// and the answer is shaped exactly like the Anthropic response.
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
type SearchHit = { title: string; url: string; snippet: string }
|
|
365
|
+
|
|
366
|
+
function decodeEntities(s: string): string {
|
|
367
|
+
return s
|
|
368
|
+
.replace(/<[^>]+>/g, '')
|
|
369
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
370
|
+
.replace(/"/g, '"').replace(/'|'/g, "'").replace(/ /g, ' ')
|
|
371
|
+
.replace(/\s+/g, ' ').trim()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function duckDuckGoSearch(query: string, signal?: AbortSignal): Promise<SearchHit[]> {
|
|
375
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`
|
|
376
|
+
const res = await fetch(url, {
|
|
377
|
+
signal,
|
|
378
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36' },
|
|
379
|
+
})
|
|
380
|
+
if (!res.ok) throw new Error(`DuckDuckGo returned HTTP ${res.status}`)
|
|
381
|
+
const html = await res.text()
|
|
382
|
+
|
|
383
|
+
const hits: SearchHit[] = []
|
|
384
|
+
const linkRe = /<a[^>]*href="([^"]+)"[^>]*class='result-link'[^>]*>([\s\S]*?)<\/a>/g
|
|
385
|
+
const snippetRe = /<td class='result-snippet'>([\s\S]*?)<\/td>/g
|
|
386
|
+
const snippets: string[] = []
|
|
387
|
+
let m: RegExpExecArray | null
|
|
388
|
+
while ((m = snippetRe.exec(html)) !== null) snippets.push(decodeEntities(m[1]!))
|
|
389
|
+
while ((m = linkRe.exec(html)) !== null) {
|
|
390
|
+
let href = m[1]!.replace(/&/g, '&')
|
|
391
|
+
const uddg = /[?&]uddg=([^&]+)/.exec(href)
|
|
392
|
+
if (uddg) href = decodeURIComponent(uddg[1]!)
|
|
393
|
+
else if (href.startsWith('//')) href = 'https:' + href
|
|
394
|
+
if (/duckduckgo\.com\/y\.js|ad_domain=/.test(href)) continue // sponsored
|
|
395
|
+
hits.push({ title: decodeEntities(m[2]!), url: href, snippet: snippets[hits.length] || '' })
|
|
396
|
+
if (hits.length >= 10) break
|
|
397
|
+
}
|
|
398
|
+
return hits
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function extractSearchQuery(params: any): string {
|
|
402
|
+
const last = [...(params.messages || [])].reverse().find((m: any) => m.role === 'user')
|
|
403
|
+
const text = last ? blockText(last.content) : ''
|
|
404
|
+
const m = /web search for the query:\s*([\s\S]+)$/i.exec(text)
|
|
405
|
+
return (m ? m[1] : text).trim()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function* webSearchStream(params: any, signal?: AbortSignal): AsyncGenerator<any> {
|
|
409
|
+
const query = extractSearchQuery(params)
|
|
410
|
+
const toolUseId = `srvtoolu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
411
|
+
|
|
412
|
+
yield { type: 'message_start', message: { id: `msg_${Date.now()}`, type: 'message', role: 'assistant', content: [], model: MODEL, stop_reason: null, stop_sequence: null, usage: emptyUsage() } }
|
|
413
|
+
|
|
414
|
+
let index = 0
|
|
415
|
+
yield { type: 'content_block_start', index, content_block: { type: 'server_tool_use', id: toolUseId, name: 'web_search', input: {} } }
|
|
416
|
+
yield { type: 'content_block_delta', index, delta: { type: 'input_json_delta', partial_json: JSON.stringify({ query }) } }
|
|
417
|
+
yield { type: 'content_block_stop', index }
|
|
418
|
+
index++
|
|
419
|
+
|
|
420
|
+
let hits: SearchHit[] = []
|
|
421
|
+
let error: string | null = null
|
|
422
|
+
try {
|
|
423
|
+
hits = await duckDuckGoSearch(query, signal)
|
|
424
|
+
} catch (e: any) {
|
|
425
|
+
error = e?.message || String(e)
|
|
426
|
+
}
|
|
427
|
+
debugLog({ web_search: query, hits: hits.length, error })
|
|
428
|
+
|
|
429
|
+
const resultBlock: any = {
|
|
430
|
+
type: 'web_search_tool_result',
|
|
431
|
+
tool_use_id: toolUseId,
|
|
432
|
+
content: error
|
|
433
|
+
? { type: 'web_search_tool_result_error', error_code: 'unavailable' }
|
|
434
|
+
: hits.map(h => ({ type: 'web_search_result', title: h.title, url: h.url, encrypted_content: '', page_age: null })),
|
|
435
|
+
}
|
|
436
|
+
yield { type: 'content_block_start', index, content_block: resultBlock }
|
|
437
|
+
yield { type: 'content_block_stop', index }
|
|
438
|
+
index++
|
|
439
|
+
|
|
440
|
+
const text = error
|
|
441
|
+
? `Web search failed: ${error}`
|
|
442
|
+
: hits.length === 0
|
|
443
|
+
? `No results found for "${query}".`
|
|
444
|
+
: `Search results for "${query}":\n\n` + hits.map((h, i) => `${i + 1}. ${h.title}\n ${h.url}${h.snippet ? `\n ${h.snippet}` : ''}`).join('\n\n')
|
|
445
|
+
yield { type: 'content_block_start', index, content_block: { type: 'text', text: '' } }
|
|
446
|
+
yield { type: 'content_block_delta', index, delta: { type: 'text_delta', text } }
|
|
447
|
+
yield { type: 'content_block_stop', index }
|
|
448
|
+
|
|
449
|
+
yield { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { input_tokens: 0, output_tokens: 0, server_tool_use: { web_search_requests: 1, web_fetch_requests: 0 } } }
|
|
450
|
+
yield { type: 'message_stop' }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function wantsWebSearch(params: any): boolean {
|
|
454
|
+
if ((process.env.OPENAI_WEB_SEARCH || '').toLowerCase() === 'off') return false
|
|
455
|
+
return (params.tools || []).some((t: any) => typeof t?.type === 'string' && t.type.startsWith('web_search'))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Create a streaming response that mimics Anthropic's stream format
|
|
459
|
+
async function* createAnthropicStyleStream(params: any, signal?: AbortSignal): AsyncGenerator<any> {
|
|
460
|
+
if (wantsWebSearch(params)) {
|
|
461
|
+
yield* webSearchStream(params, signal)
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const openai = getOpenAI()
|
|
466
|
+
const requestParams = buildRequest(params, true)
|
|
467
|
+
debugLog({ request: summarizeRequest(requestParams) })
|
|
240
468
|
|
|
241
469
|
let stream: any
|
|
242
470
|
try {
|
|
243
471
|
stream = await openai.chat.completions.create(requestParams, { signal })
|
|
244
472
|
} catch (err: any) {
|
|
245
|
-
|
|
246
|
-
const { APIError } = await import('@anthropic-ai/sdk')
|
|
247
|
-
const status = err?.status || 500
|
|
248
|
-
throw new APIError(status, err?.error || {}, err?.message || 'OpenAI API error', new Headers())
|
|
473
|
+
throw await toAnthropicError(err)
|
|
249
474
|
}
|
|
250
475
|
|
|
251
476
|
// Emit message_start
|
|
252
|
-
const messageId = `msg_${Date.now()}`
|
|
253
477
|
yield {
|
|
254
478
|
type: 'message_start',
|
|
255
479
|
message: {
|
|
256
|
-
id:
|
|
480
|
+
id: `msg_${Date.now()}`,
|
|
257
481
|
type: 'message',
|
|
258
482
|
role: 'assistant',
|
|
259
483
|
content: [],
|
|
260
|
-
model,
|
|
484
|
+
model: MODEL,
|
|
261
485
|
stop_reason: null,
|
|
262
486
|
stop_sequence: null,
|
|
263
|
-
usage:
|
|
264
|
-
input_tokens: 0,
|
|
265
|
-
output_tokens: 0,
|
|
266
|
-
cache_creation_input_tokens: 0,
|
|
267
|
-
cache_read_input_tokens: 0,
|
|
268
|
-
server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },
|
|
269
|
-
service_tier: 'standard',
|
|
270
|
-
cache_creation: {
|
|
271
|
-
ephemeral_1h_input_tokens: 0,
|
|
272
|
-
ephemeral_5m_input_tokens: 0,
|
|
273
|
-
},
|
|
274
|
-
inference_geo: '',
|
|
275
|
-
iterations: [],
|
|
276
|
-
speed: 'standard',
|
|
277
|
-
},
|
|
487
|
+
usage: emptyUsage(),
|
|
278
488
|
},
|
|
279
489
|
}
|
|
280
490
|
|
|
491
|
+
// One content block is open at a time: thinking, text, or a tool call
|
|
281
492
|
let contentBlockIndex = 0
|
|
282
|
-
let
|
|
283
|
-
let currentToolName: string | null = null
|
|
493
|
+
let openBlock: 'thinking' | 'text' | 'tool_use' | null = null
|
|
284
494
|
let toolInputAccum = ''
|
|
285
|
-
|
|
495
|
+
// OpenAI streams parallel tool calls keyed by index; the id only arrives on the first chunk
|
|
496
|
+
let currentToolIndex: number | null = null
|
|
497
|
+
let finishReason: string | null = null
|
|
498
|
+
let usage: any = null
|
|
499
|
+
|
|
500
|
+
function* closeBlock(): Generator<any> {
|
|
501
|
+
if (!openBlock) return
|
|
502
|
+
if (openBlock === 'tool_use' && toolInputAccum.trim() === '') {
|
|
503
|
+
// Tools called without arguments must still carry a valid JSON object
|
|
504
|
+
yield {
|
|
505
|
+
type: 'content_block_delta',
|
|
506
|
+
index: contentBlockIndex,
|
|
507
|
+
delta: { type: 'input_json_delta', partial_json: '{}' },
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
yield { type: 'content_block_stop', index: contentBlockIndex }
|
|
511
|
+
contentBlockIndex++
|
|
512
|
+
openBlock = null
|
|
513
|
+
currentToolIndex = null
|
|
514
|
+
toolInputAccum = ''
|
|
515
|
+
}
|
|
286
516
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
517
|
+
function* openBlockOf(type: 'thinking' | 'text'): Generator<any> {
|
|
518
|
+
if (openBlock === type) return
|
|
519
|
+
yield* closeBlock()
|
|
520
|
+
openBlock = type
|
|
521
|
+
yield {
|
|
522
|
+
type: 'content_block_start',
|
|
523
|
+
index: contentBlockIndex,
|
|
524
|
+
content_block: type === 'thinking'
|
|
525
|
+
? { type: 'thinking', thinking: '', signature: '' }
|
|
526
|
+
: { type: 'text', text: '' },
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
for await (const chunk of stream) {
|
|
532
|
+
// The usage chunk arrives last, with an empty choices array
|
|
533
|
+
if (chunk.usage) usage = chunk.usage
|
|
534
|
+
|
|
535
|
+
const choice = chunk.choices?.[0]
|
|
536
|
+
if (!choice) continue
|
|
290
537
|
|
|
291
|
-
|
|
538
|
+
const delta: any = choice.delta
|
|
292
539
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
540
|
+
// Reasoning models: llama.cpp/DeepSeek send reasoning_content, OpenRouter sends reasoning
|
|
541
|
+
const reasoning = delta?.reasoning_content ?? delta?.reasoning
|
|
542
|
+
if (typeof reasoning === 'string' && reasoning) {
|
|
543
|
+
yield* openBlockOf('thinking')
|
|
297
544
|
yield {
|
|
298
|
-
type: '
|
|
545
|
+
type: 'content_block_delta',
|
|
299
546
|
index: contentBlockIndex,
|
|
300
|
-
|
|
547
|
+
delta: { type: 'thinking_delta', thinking: reasoning },
|
|
301
548
|
}
|
|
302
549
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
550
|
+
|
|
551
|
+
// Handle text content
|
|
552
|
+
if (delta?.content) {
|
|
553
|
+
yield* openBlockOf('text')
|
|
554
|
+
yield {
|
|
555
|
+
type: 'content_block_delta',
|
|
556
|
+
index: contentBlockIndex,
|
|
557
|
+
delta: { type: 'text_delta', text: delta.content },
|
|
558
|
+
}
|
|
307
559
|
}
|
|
308
|
-
}
|
|
309
560
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
hasTextBlock = false
|
|
319
|
-
}
|
|
320
|
-
// Close previous tool call if open
|
|
321
|
-
if (currentToolCallId) {
|
|
561
|
+
// Handle tool calls
|
|
562
|
+
if (delta?.tool_calls) {
|
|
563
|
+
for (const tc of delta.tool_calls) {
|
|
564
|
+
const index = tc.index ?? 0
|
|
565
|
+
if (openBlock !== 'tool_use' || index !== currentToolIndex) {
|
|
566
|
+
yield* closeBlock()
|
|
567
|
+
openBlock = 'tool_use'
|
|
568
|
+
currentToolIndex = index
|
|
322
569
|
yield {
|
|
323
|
-
type: '
|
|
570
|
+
type: 'content_block_start',
|
|
324
571
|
index: contentBlockIndex,
|
|
325
|
-
|
|
572
|
+
content_block: {
|
|
573
|
+
type: 'tool_use',
|
|
574
|
+
id: tc.id || `call_${Date.now()}_${index}`,
|
|
575
|
+
name: tc.function?.name || '',
|
|
576
|
+
input: '',
|
|
577
|
+
},
|
|
326
578
|
}
|
|
327
|
-
yield { type: 'content_block_stop', index: contentBlockIndex }
|
|
328
|
-
contentBlockIndex++
|
|
329
579
|
}
|
|
330
580
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
content_block: {
|
|
339
|
-
type: 'tool_use',
|
|
340
|
-
id: tc.id,
|
|
341
|
-
name: currentToolName,
|
|
342
|
-
input: "",
|
|
343
|
-
},
|
|
581
|
+
if (tc.function?.arguments) {
|
|
582
|
+
toolInputAccum += tc.function.arguments
|
|
583
|
+
yield {
|
|
584
|
+
type: 'content_block_delta',
|
|
585
|
+
index: contentBlockIndex,
|
|
586
|
+
delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
|
|
587
|
+
}
|
|
344
588
|
}
|
|
345
589
|
}
|
|
590
|
+
}
|
|
346
591
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
yield {
|
|
350
|
-
type: 'content_block_delta',
|
|
351
|
-
index: contentBlockIndex,
|
|
352
|
-
delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
|
|
353
|
-
}
|
|
354
|
-
}
|
|
592
|
+
if (choice.finish_reason) {
|
|
593
|
+
finishReason = choice.finish_reason
|
|
355
594
|
}
|
|
356
595
|
}
|
|
596
|
+
} catch (err: any) {
|
|
597
|
+
throw await toAnthropicError(err)
|
|
598
|
+
}
|
|
357
599
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (hasTextBlock && !currentToolCallId) {
|
|
362
|
-
yield { type: 'content_block_stop', index: contentBlockIndex }
|
|
363
|
-
contentBlockIndex++
|
|
364
|
-
}
|
|
365
|
-
if (currentToolCallId) {
|
|
366
|
-
yield { type: 'content_block_stop', index: contentBlockIndex }
|
|
367
|
-
contentBlockIndex++
|
|
368
|
-
}
|
|
600
|
+
// Close blocks and report stop reason + usage only after the stream ends,
|
|
601
|
+
// so the trailing usage chunk is included
|
|
602
|
+
yield* closeBlock()
|
|
369
603
|
|
|
370
|
-
|
|
371
|
-
type: 'message_delta',
|
|
372
|
-
delta: {
|
|
373
|
-
stop_reason: mapStopReason(choice.finish_reason),
|
|
374
|
-
stop_sequence: null,
|
|
375
|
-
},
|
|
376
|
-
usage: { output_tokens: chunk.usage?.completion_tokens || 0 },
|
|
377
|
-
}
|
|
604
|
+
debugLog({ response: { stop: finishReason, usage } })
|
|
378
605
|
|
|
379
|
-
|
|
380
|
-
|
|
606
|
+
yield {
|
|
607
|
+
type: 'message_delta',
|
|
608
|
+
delta: {
|
|
609
|
+
stop_reason: mapStopReason(finishReason),
|
|
610
|
+
stop_sequence: null,
|
|
611
|
+
},
|
|
612
|
+
usage: {
|
|
613
|
+
input_tokens: usage?.prompt_tokens || 0,
|
|
614
|
+
output_tokens: usage?.completion_tokens || 0,
|
|
615
|
+
},
|
|
381
616
|
}
|
|
617
|
+
|
|
618
|
+
yield { type: 'message_stop' }
|
|
382
619
|
}
|
|
383
620
|
|
|
384
621
|
// Create a fake Anthropic client that uses OpenAI
|
|
@@ -391,8 +628,13 @@ export function createOpenAIBackedAnthropicClient(): any {
|
|
|
391
628
|
// Return an APIPromise-like object: a Promise with .withResponse()
|
|
392
629
|
const abortController = new AbortController()
|
|
393
630
|
const generator = createAnthropicStyleStream(params, options?.signal || abortController.signal)
|
|
631
|
+
let iterators = 0
|
|
394
632
|
const streamObj: any = {
|
|
395
|
-
[Symbol.asyncIterator]: () =>
|
|
633
|
+
[Symbol.asyncIterator]: () => {
|
|
634
|
+
iterators++
|
|
635
|
+
debugLog({ stream_iterator: iterators })
|
|
636
|
+
return generator
|
|
637
|
+
},
|
|
396
638
|
controller: abortController,
|
|
397
639
|
}
|
|
398
640
|
|
|
@@ -407,86 +649,60 @@ export function createOpenAIBackedAnthropicClient(): any {
|
|
|
407
649
|
return promise
|
|
408
650
|
}
|
|
409
651
|
|
|
410
|
-
// Non-streaming
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// Convert to Anthropic format
|
|
447
|
-
const content: any[] = []
|
|
448
|
-
if (choice?.message?.content) {
|
|
449
|
-
content.push({ type: 'text', text: choice.message.content })
|
|
450
|
-
}
|
|
451
|
-
if (choice?.message?.tool_calls) {
|
|
452
|
-
for (const tc of choice.message.tool_calls) {
|
|
453
|
-
let input = {}
|
|
454
|
-
try { input = JSON.parse(tc.function.arguments) } catch {}
|
|
455
|
-
content.push({
|
|
456
|
-
type: 'tool_use',
|
|
457
|
-
id: tc.id,
|
|
458
|
-
name: tc.function.name,
|
|
459
|
-
input,
|
|
460
|
-
})
|
|
652
|
+
// Non-streaming
|
|
653
|
+
return (async () => {
|
|
654
|
+
const openai = getOpenAI()
|
|
655
|
+
const requestParams = buildRequest(params, false)
|
|
656
|
+
debugLog({ request: summarizeRequest(requestParams) })
|
|
657
|
+
|
|
658
|
+
let response: any
|
|
659
|
+
try {
|
|
660
|
+
response = await openai.chat.completions.create(requestParams, { signal: options?.signal })
|
|
661
|
+
} catch (err: any) {
|
|
662
|
+
throw await toAnthropicError(err)
|
|
663
|
+
}
|
|
664
|
+
const choice = response.choices[0]
|
|
665
|
+
const message: any = choice?.message || {}
|
|
666
|
+
|
|
667
|
+
// Convert to Anthropic format
|
|
668
|
+
const content: any[] = []
|
|
669
|
+
const reasoning = message.reasoning_content ?? message.reasoning
|
|
670
|
+
if (typeof reasoning === 'string' && reasoning) {
|
|
671
|
+
content.push({ type: 'thinking', thinking: reasoning, signature: '' })
|
|
672
|
+
}
|
|
673
|
+
if (message.content) {
|
|
674
|
+
content.push({ type: 'text', text: message.content })
|
|
675
|
+
}
|
|
676
|
+
if (message.tool_calls) {
|
|
677
|
+
for (const tc of message.tool_calls) {
|
|
678
|
+
let input = {}
|
|
679
|
+
try { input = JSON.parse(tc.function.arguments) } catch {}
|
|
680
|
+
content.push({
|
|
681
|
+
type: 'tool_use',
|
|
682
|
+
id: tc.id,
|
|
683
|
+
name: tc.function.name,
|
|
684
|
+
input,
|
|
685
|
+
})
|
|
686
|
+
}
|
|
461
687
|
}
|
|
462
|
-
}
|
|
463
688
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
service_tier: 'standard',
|
|
479
|
-
cache_creation: {
|
|
480
|
-
ephemeral_1h_input_tokens: 0,
|
|
481
|
-
ephemeral_5m_input_tokens: 0,
|
|
689
|
+
debugLog({ response: { stop: choice?.finish_reason, usage: response.usage } })
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
id: messageId(),
|
|
693
|
+
type: 'message',
|
|
694
|
+
role: 'assistant',
|
|
695
|
+
content,
|
|
696
|
+
model: MODEL,
|
|
697
|
+
stop_reason: mapStopReason(choice?.finish_reason || null),
|
|
698
|
+
stop_sequence: null,
|
|
699
|
+
usage: {
|
|
700
|
+
...emptyUsage(),
|
|
701
|
+
input_tokens: response.usage?.prompt_tokens || 0,
|
|
702
|
+
output_tokens: response.usage?.completion_tokens || 0,
|
|
482
703
|
},
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
speed: 'standard',
|
|
486
|
-
},
|
|
487
|
-
}
|
|
488
|
-
})(); // end async IIFE
|
|
489
|
-
return nonStreamingResult;
|
|
704
|
+
}
|
|
705
|
+
})()
|
|
490
706
|
},
|
|
491
707
|
}
|
|
492
708
|
|