@vellumai/assistant 0.11.10 → 0.11.11-staging.1
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/AGENTS.md +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/openapi.yaml +152 -55
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +60 -0
- package/src/__tests__/config-get-vision-flag.test.ts +36 -0
- package/src/__tests__/conversation-error.test.ts +38 -0
- package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
- package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
- package/src/__tests__/headless-browser-mode.test.ts +63 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +118 -27
- package/src/__tests__/host-app-control-proxy.test.ts +2 -2
- package/src/__tests__/host-app-control-routes.test.ts +41 -0
- package/src/__tests__/host-bash-routes.test.ts +30 -1
- package/src/__tests__/host-browser-routes.test.ts +34 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
- package/src/__tests__/host-proxy-base.test.ts +1 -0
- package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
- package/src/__tests__/llm-schema.test.ts +32 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
- package/src/__tests__/memory-config-file-parity.test.ts +18 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
- package/src/__tests__/messaging-send-tool.test.ts +159 -13
- package/src/__tests__/messaging-shared.test.ts +19 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
- package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
- package/src/__tests__/schedule-routes.test.ts +30 -1
- package/src/__tests__/scheduler-result-notification.test.ts +209 -0
- package/src/__tests__/skills-install-extract.test.ts +26 -0
- package/src/__tests__/skills-install-staging.test.ts +11 -2
- package/src/api/constants/personality-sliders.test.ts +21 -0
- package/src/api/constants/personality-sliders.ts +64 -0
- package/src/api/index.ts +8 -0
- package/src/archive/ustar-size.test.ts +32 -0
- package/src/archive/ustar-size.ts +23 -0
- package/src/background-wake/next-wake.test.ts +13 -0
- package/src/background-wake/next-wake.ts +4 -2
- package/src/calls/voice-session-bridge.ts +26 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
- package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
- package/src/cli/commands/config.help.ts +3 -1
- package/src/cli/commands/memory/worker.ts +1 -1
- package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
- package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
- package/src/cli/commands/platform/credits-format.ts +57 -0
- package/src/cli/commands/platform/index.help.ts +19 -3
- package/src/cli/commands/plugins.help.ts +4 -1
- package/src/cli/commands/plugins.ts +2 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-platform.ts +8 -10
- package/src/config/bundled-skills/messaging/SKILL.md +2 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +9 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/input-modalities.test.ts +71 -0
- package/src/config/input-modalities.ts +95 -0
- package/src/config/schemas/heartbeat.ts +3 -3
- package/src/config/schemas/llm.ts +13 -3
- package/src/config/schemas/memory-storage.test.ts +39 -0
- package/src/config/schemas/memory-storage.ts +21 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
- package/src/daemon/conversation-error.ts +20 -7
- package/src/daemon/embedding-reconcile.ts +64 -1
- package/src/daemon/handlers/config-embeddings.ts +31 -0
- package/src/daemon/host-app-control-proxy.ts +2 -0
- package/src/daemon/host-bash-proxy.ts +1 -0
- package/src/daemon/host-cu-proxy.ts +6 -6
- package/src/daemon/host-file-proxy.ts +6 -6
- package/src/daemon/host-proxy-base.ts +10 -9
- package/src/daemon/host-transfer-proxy.ts +21 -24
- package/src/daemon/mcp-reload-service.ts +6 -0
- package/src/daemon/providers-setup.ts +5 -26
- package/src/heartbeat/heartbeat-service.ts +72 -13
- package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
- package/src/live-voice/live-voice-photo.ts +68 -4
- package/src/live-voice/live-voice-session.ts +36 -2
- package/src/live-voice/protocol.ts +98 -1
- package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
- package/src/mcp/__tests__/reload-signal.test.ts +57 -0
- package/src/mcp/__tests__/startup.test.ts +150 -0
- package/src/mcp/reload-signal.ts +50 -0
- package/src/mcp/startup.ts +101 -0
- package/src/messaging/providers/outlook/adapter.ts +2 -13
- package/src/messaging/providers/outlook/client.ts +13 -1
- package/src/messaging/providers/outlook/types.ts +3 -0
- package/src/notifications/__tests__/decision-engine.test.ts +79 -6
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
- package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
- package/src/notifications/copy-composer.ts +33 -11
- package/src/notifications/decision-engine.ts +48 -11
- package/src/notifications/events-store.ts +68 -2
- package/src/notifications/home-feed-side-effect.ts +15 -3
- package/src/notifications/schedule-result-producer.ts +275 -0
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/seed-providers.ts +3 -2
- package/src/persistence/conversation-crud.ts +38 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
- package/src/persistence/embeddings/embedding-backend.ts +148 -11
- package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
- package/src/persistence/embeddings/embedding-openai.ts +55 -4
- package/src/persistence/embeddings/embedding-types.test.ts +24 -0
- package/src/persistence/embeddings/embedding-types.ts +25 -1
- package/src/persistence/job-utils.ts +6 -2
- package/src/plugin-api/index.ts +2 -1
- package/src/plugin-api/vision-support.test.ts +81 -0
- package/src/plugin-api/vision-support.ts +57 -10
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
- package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
- package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
- package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
- package/src/providers/anthropic/client.ts +9 -4
- package/src/providers/inline-audio-support.test.ts +34 -0
- package/src/providers/inline-audio-support.ts +27 -0
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
- package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
- package/src/providers/openai/api-error-normalization.ts +18 -3
- package/src/providers/openai/chat-completions-provider.ts +113 -13
- package/src/providers/openai/responses-provider.ts +10 -8
- package/src/providers/provider-secret-catalog.ts +11 -1
- package/src/providers/vellum/client.ts +14 -1
- package/src/providers/vellum/personality-directions.test.ts +163 -0
- package/src/providers/vellum/personality-directions.ts +120 -0
- package/src/providers/xml-tool-call-salvage.ts +212 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
- package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
- package/src/runtime/auth/same-actor.ts +29 -5
- package/src/runtime/background-job-runner.ts +38 -29
- package/src/runtime/guardian-action-service.ts +20 -1
- package/src/runtime/migrations/vbundle-validator.ts +3 -3
- package/src/runtime/pending-interactions.ts +4 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
- package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
- package/src/runtime/routes/conversation-query-routes.ts +53 -11
- package/src/runtime/routes/document-comments-routes.ts +8 -2
- package/src/runtime/routes/guardian-action-routes.ts +7 -0
- package/src/runtime/routes/heartbeat-routes.ts +41 -53
- package/src/runtime/routes/host-app-control-routes.ts +10 -36
- package/src/runtime/routes/host-bash-routes.ts +11 -45
- package/src/runtime/routes/host-browser-routes.ts +24 -0
- package/src/runtime/routes/host-cu-routes.ts +11 -45
- package/src/runtime/routes/host-file-routes.ts +11 -44
- package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
- package/src/runtime/routes/host-transfer-routes.ts +33 -25
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/integrations/a2a.ts +12 -3
- package/src/runtime/routes/integrations/vercel.ts +13 -3
- package/src/runtime/routes/platform-routes.ts +121 -24
- package/src/runtime/routes/schedule-routes.ts +15 -1
- package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
- package/src/schedule/schedule-timezone.ts +25 -0
- package/src/schedule/scheduler.ts +19 -0
- package/src/schedule/worker.ts +119 -2
- package/src/skills/catalog-install.ts +40 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
- package/src/tools/browser/browser-execution.ts +117 -53
- package/src/tools/browser/browser-status-constants.ts +6 -6
- package/src/tools/capability-offer.test.ts +87 -0
- package/src/tools/capability-offer.ts +93 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/util/provider-error-patterns.ts +34 -0
- package/src/watcher/__tests__/engine.test.ts +5 -2
- package/src/watcher/engine.ts +7 -4
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
- package/src/runtime/routes/sight-frame-routes.ts +0 -136
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
OpenAIChatCompletionsProvider,
|
|
5
|
+
type OpenAIChatCompletionsProviderOptions,
|
|
6
|
+
} from "../chat-completions-provider.js";
|
|
7
|
+
|
|
8
|
+
type MockChunkDelta = {
|
|
9
|
+
content?: string | null;
|
|
10
|
+
tool_calls?: Array<{
|
|
11
|
+
index: number;
|
|
12
|
+
id?: string;
|
|
13
|
+
function?: { name?: string; arguments?: string };
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type MockChunk = {
|
|
18
|
+
choices: Array<{ delta: MockChunkDelta; finish_reason?: string | null }>;
|
|
19
|
+
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const BASH_TOOL = {
|
|
23
|
+
name: "bash",
|
|
24
|
+
description: "Run a shell command",
|
|
25
|
+
input_schema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
command: { type: "string" },
|
|
29
|
+
activity: { type: "string" },
|
|
30
|
+
timeout_seconds: { type: "number" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const INVOKE_XML = `<invoke name="bash">
|
|
36
|
+
<parameter name="command">cd /workspace/example && git status</parameter>
|
|
37
|
+
<parameter name="activity">Checking live repo state</parameter>
|
|
38
|
+
<parameter name="timeout_seconds">60</parameter>
|
|
39
|
+
</invoke>`;
|
|
40
|
+
|
|
41
|
+
function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
|
|
42
|
+
return {
|
|
43
|
+
async *[Symbol.asyncIterator]() {
|
|
44
|
+
for (const c of chunks) {
|
|
45
|
+
yield c;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stubProvider(
|
|
52
|
+
chunks: MockChunk[],
|
|
53
|
+
options?: OpenAIChatCompletionsProviderOptions,
|
|
54
|
+
model = "test-model",
|
|
55
|
+
): {
|
|
56
|
+
provider: OpenAIChatCompletionsProvider;
|
|
57
|
+
events: Array<{ type: string; text?: string }>;
|
|
58
|
+
} {
|
|
59
|
+
const provider = new OpenAIChatCompletionsProvider("test-key", model, options);
|
|
60
|
+
(provider as unknown as { client: unknown }).client = {
|
|
61
|
+
chat: {
|
|
62
|
+
completions: {
|
|
63
|
+
create: async () => makeStream(chunks),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return { provider, events: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contentChunks(parts: string[], finishReason = "stop"): MockChunk[] {
|
|
71
|
+
return [
|
|
72
|
+
...parts.map((content) => ({
|
|
73
|
+
choices: [{ delta: { content } }],
|
|
74
|
+
})),
|
|
75
|
+
{
|
|
76
|
+
choices: [{ delta: {}, finish_reason: finishReason }],
|
|
77
|
+
usage: { prompt_tokens: 1, completion_tokens: 2 },
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("OpenAIChatCompletionsProvider XML tool-call salvage", () => {
|
|
83
|
+
test("converts streamed invoke XML into a tool_use block and holds it back from text_delta", async () => {
|
|
84
|
+
const { provider, events } = stubProvider(
|
|
85
|
+
contentChunks([
|
|
86
|
+
"Let me actually run the check.\n\n",
|
|
87
|
+
"<inv",
|
|
88
|
+
`oke name="bash">\n`,
|
|
89
|
+
`<parameter name="command">cd /workspace/example && git status</parameter>\n`,
|
|
90
|
+
`<parameter name="activity">Checking live repo state</parameter>\n`,
|
|
91
|
+
`<parameter name="timeout_seconds">60</parameter>\n`,
|
|
92
|
+
"</invoke>",
|
|
93
|
+
]),
|
|
94
|
+
{ salvageXmlToolCalls: true },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const response = await provider.sendMessage(
|
|
98
|
+
[{ role: "user", content: [{ type: "text", text: "check the repo" }] }],
|
|
99
|
+
{
|
|
100
|
+
tools: [BASH_TOOL],
|
|
101
|
+
onEvent: (e) => {
|
|
102
|
+
events.push(e as { type: string; text?: string });
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const textDeltas = events
|
|
108
|
+
.filter((e) => e.type === "text_delta")
|
|
109
|
+
.map((e) => e.text ?? "")
|
|
110
|
+
.join("");
|
|
111
|
+
expect(textDeltas).toBe("Let me actually run the check.\n\n");
|
|
112
|
+
expect(textDeltas).not.toContain("<invoke");
|
|
113
|
+
expect(textDeltas).not.toContain("<inv");
|
|
114
|
+
|
|
115
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
116
|
+
| { type: "text"; text: string }
|
|
117
|
+
| undefined;
|
|
118
|
+
expect(textBlock?.text).toBe("Let me actually run the check.");
|
|
119
|
+
|
|
120
|
+
const toolUse = response.content.filter((b) => b.type === "tool_use");
|
|
121
|
+
expect(toolUse).toEqual([
|
|
122
|
+
{
|
|
123
|
+
type: "tool_use",
|
|
124
|
+
id: expect.stringMatching(/^call_/),
|
|
125
|
+
name: "bash",
|
|
126
|
+
input: {
|
|
127
|
+
command: "cd /workspace/example && git status",
|
|
128
|
+
activity: "Checking live repo state",
|
|
129
|
+
timeout_seconds: 60,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
expect(response.stopReason).toBe("tool_calls");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("salvages invoke XML when parseThinkTags is enabled", async () => {
|
|
137
|
+
const { provider } = stubProvider(
|
|
138
|
+
contentChunks([`Okay.\n${INVOKE_XML}`]),
|
|
139
|
+
{ parseThinkTags: true, salvageXmlToolCalls: true },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const response = await provider.sendMessage(
|
|
143
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
144
|
+
{ tools: [BASH_TOOL] },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(true);
|
|
148
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
149
|
+
| { type: "text"; text: string }
|
|
150
|
+
| undefined;
|
|
151
|
+
expect(textBlock?.text).toBe("Okay.");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("leaves XML as text when native tool_calls are already present", async () => {
|
|
155
|
+
const { provider } = stubProvider(
|
|
156
|
+
[
|
|
157
|
+
{ choices: [{ delta: { content: `${INVOKE_XML}\n` } }] },
|
|
158
|
+
{
|
|
159
|
+
choices: [
|
|
160
|
+
{
|
|
161
|
+
delta: {
|
|
162
|
+
tool_calls: [
|
|
163
|
+
{
|
|
164
|
+
index: 0,
|
|
165
|
+
id: "call_native",
|
|
166
|
+
function: {
|
|
167
|
+
name: "bash",
|
|
168
|
+
arguments: '{"command":"pwd"}',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
choices: [{ delta: {}, finish_reason: "tool_calls" }],
|
|
178
|
+
usage: { prompt_tokens: 1, completion_tokens: 2 },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
{ salvageXmlToolCalls: true },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const response = await provider.sendMessage(
|
|
185
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
186
|
+
{ tools: [BASH_TOOL] },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const toolUse = response.content.filter((b) => b.type === "tool_use");
|
|
190
|
+
expect(toolUse).toHaveLength(1);
|
|
191
|
+
expect(toolUse[0]).toMatchObject({
|
|
192
|
+
id: "call_native",
|
|
193
|
+
name: "bash",
|
|
194
|
+
input: { command: "pwd" },
|
|
195
|
+
});
|
|
196
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
197
|
+
| { type: "text"; text: string }
|
|
198
|
+
| undefined;
|
|
199
|
+
expect(textBlock?.text).toContain("<invoke");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("leaves XML as text when no tools were offered", async () => {
|
|
203
|
+
const { provider, events } = stubProvider(contentChunks([INVOKE_XML]));
|
|
204
|
+
|
|
205
|
+
const response = await provider.sendMessage(
|
|
206
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
207
|
+
{
|
|
208
|
+
onEvent: (e) => {
|
|
209
|
+
events.push(e as { type: string; text?: string });
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(false);
|
|
215
|
+
expect(
|
|
216
|
+
events
|
|
217
|
+
.filter((e) => e.type === "text_delta")
|
|
218
|
+
.map((e) => e.text ?? "")
|
|
219
|
+
.join(""),
|
|
220
|
+
).toContain("<invoke");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("leaves XML as text when tool_choice is none", async () => {
|
|
224
|
+
const { provider } = stubProvider(contentChunks([INVOKE_XML]), {
|
|
225
|
+
salvageXmlToolCalls: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const response = await provider.sendMessage(
|
|
229
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
230
|
+
{
|
|
231
|
+
tools: [BASH_TOOL],
|
|
232
|
+
config: { tool_choice: { type: "none" } },
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(false);
|
|
237
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
238
|
+
| { type: "text"; text: string }
|
|
239
|
+
| undefined;
|
|
240
|
+
expect(textBlock?.text).toContain("<invoke");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("leaves XML as text on non-DeepSeek models unless salvage is opted in", async () => {
|
|
244
|
+
const { provider } = stubProvider(contentChunks([INVOKE_XML]));
|
|
245
|
+
|
|
246
|
+
const response = await provider.sendMessage(
|
|
247
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
248
|
+
{ tools: [BASH_TOOL] },
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(false);
|
|
252
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
253
|
+
| { type: "text"; text: string }
|
|
254
|
+
| undefined;
|
|
255
|
+
expect(textBlock?.text).toContain("<invoke");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("salvages by default when the model id is DeepSeek", async () => {
|
|
259
|
+
const { provider } = stubProvider(
|
|
260
|
+
contentChunks([INVOKE_XML]),
|
|
261
|
+
undefined,
|
|
262
|
+
"accounts/fireworks/models/deepseek-v4-flash-0731",
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const response = await provider.sendMessage(
|
|
266
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
267
|
+
{ tools: [BASH_TOOL] },
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("does not salvage DeepSeek when salvageXmlToolCalls is false", async () => {
|
|
274
|
+
const { provider } = stubProvider(
|
|
275
|
+
contentChunks([INVOKE_XML]),
|
|
276
|
+
{ salvageXmlToolCalls: false },
|
|
277
|
+
"accounts/fireworks/models/deepseek-v4-flash-0731",
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const response = await provider.sendMessage(
|
|
281
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
282
|
+
{ tools: [BASH_TOOL] },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(response.content.some((b) => b.type === "tool_use")).toBe(false);
|
|
286
|
+
const textBlock = response.content.find((b) => b.type === "text") as
|
|
287
|
+
| { type: "text"; text: string }
|
|
288
|
+
| undefined;
|
|
289
|
+
expect(textBlock?.text).toContain("<invoke");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
DAILY_LIMIT_PATTERNS,
|
|
6
6
|
INSUFFICIENT_CREDITS_PATTERNS,
|
|
7
7
|
isChatTemplateFailureError,
|
|
8
|
+
MODEL_NOT_FOUND_PATTERNS,
|
|
9
|
+
UNSUPPORTED_MODEL_ID_PATTERNS,
|
|
8
10
|
VISION_NOT_SUPPORTED_PATTERNS,
|
|
9
11
|
} from "../../util/provider-error-patterns.js";
|
|
10
12
|
|
|
@@ -52,8 +54,8 @@ export function normalizedErrorText(n: NormalizedOpenAIAPIError): string {
|
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Map an OpenAI-compatible error to a semantic {@link ProviderErrorReason}.
|
|
55
|
-
* Order matters
|
|
56
|
-
* credential branch, and billing precedes credentials.
|
|
57
|
+
* Order matters: model restriction and model-not-found precede the generic
|
|
58
|
+
* 401/403 credential branch, and billing precedes credentials.
|
|
57
59
|
*/
|
|
58
60
|
export function deriveReason(
|
|
59
61
|
n: NormalizedOpenAIAPIError,
|
|
@@ -72,12 +74,25 @@ export function deriveReason(
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
if (
|
|
75
|
-
|
|
77
|
+
MODEL_NOT_FOUND_PATTERNS.some((re) => re.test(haystack)) ||
|
|
76
78
|
/model_not_found/i.test(`${n.apiErrorCode ?? ""} ${n.apiErrorType ?? ""}`)
|
|
77
79
|
) {
|
|
78
80
|
return "model_not_found";
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
// OpenCode's unsupported-model payload uses type=ModelError on a 401.
|
|
84
|
+
// Gate on 4xx so a 5xx that happens to carry that type stays server_error
|
|
85
|
+
// (retryable) instead of model_not_found (not retryable, fallback-eligible).
|
|
86
|
+
if (
|
|
87
|
+
status !== undefined &&
|
|
88
|
+
status >= 400 &&
|
|
89
|
+
status < 500 &&
|
|
90
|
+
(UNSUPPORTED_MODEL_ID_PATTERNS.some((re) => re.test(haystack)) ||
|
|
91
|
+
/ModelError/i.test(`${n.apiErrorCode ?? ""} ${n.apiErrorType ?? ""}`))
|
|
92
|
+
) {
|
|
93
|
+
return "model_not_found";
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
if (VISION_NOT_SUPPORTED_PATTERNS.some((re) => re.test(haystack))) {
|
|
82
97
|
return "vision_unsupported";
|
|
83
98
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import OpenAI from "openai";
|
|
2
4
|
|
|
3
5
|
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
@@ -9,12 +11,12 @@ import { extractRetryAfterMs } from "../../util/retry.js";
|
|
|
9
11
|
import { partialTagSuffix as sharedPartialTagSuffix } from "../../util/think-tag-stream.js";
|
|
10
12
|
import { clampProviderString } from "../content-block-size.js";
|
|
11
13
|
import { fileBlockToProviderText } from "../file-block-text.js";
|
|
14
|
+
import { requestSupportsInlineAudio } from "../inline-audio-support.js";
|
|
12
15
|
import {
|
|
13
16
|
base64Source,
|
|
14
17
|
mediaSourceByteLength,
|
|
15
18
|
resolveMediaReferences,
|
|
16
19
|
} from "../media-resolve.js";
|
|
17
|
-
import { modelSupportsAudioInput } from "../model-catalog.js";
|
|
18
20
|
import { PLACEHOLDER_EMPTY_TURN } from "../placeholder-sentinels.js";
|
|
19
21
|
import { recordProviderRequestDiagnostics } from "../request-diagnostics.js";
|
|
20
22
|
import { createStreamTimeout } from "../stream-timeout.js";
|
|
@@ -34,6 +36,11 @@ import {
|
|
|
34
36
|
isUnparseableToolArgs,
|
|
35
37
|
wrapUnparseableToolArgs,
|
|
36
38
|
} from "../unparseable-tool-args.js";
|
|
39
|
+
import {
|
|
40
|
+
salvageXmlToolCalls,
|
|
41
|
+
shouldSalvageXmlToolCalls,
|
|
42
|
+
splitXmlToolCallHoldback,
|
|
43
|
+
} from "../xml-tool-call-salvage.js";
|
|
37
44
|
import {
|
|
38
45
|
captureRawErrorBodyFetch,
|
|
39
46
|
formatNormalizedOpenAIAPIError,
|
|
@@ -177,6 +184,12 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
177
184
|
* with minimax-m3 on Fireworks). Off by default; scalars/arrays unaffected.
|
|
178
185
|
* See {@link coerceObjectParamsToJsonString}. */
|
|
179
186
|
coerceObjectArgsToJsonString?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Convert complete `<invoke>` XML in assistant text into `tool_use` blocks.
|
|
189
|
+
* Defaults to on for DeepSeek model ids and off otherwise. Set explicitly to
|
|
190
|
+
* override the model-id default.
|
|
191
|
+
*/
|
|
192
|
+
salvageXmlToolCalls?: boolean;
|
|
180
193
|
/** Drop `tool_choice` when thinking/reasoning is on the wire. Strict
|
|
181
194
|
* OpenAI-compatible reasoning upstreams (DeepSeek thinking mode) reject any
|
|
182
195
|
* explicit `tool_choice` with `Thinking mode does not support this
|
|
@@ -692,8 +705,11 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
692
705
|
private requestHeaders: Record<string, string>;
|
|
693
706
|
private parseThinkTags: boolean;
|
|
694
707
|
private assistantReasoningField:
|
|
695
|
-
|
|
708
|
+
| "reasoning"
|
|
709
|
+
| "reasoning_content"
|
|
710
|
+
| undefined;
|
|
696
711
|
private coerceObjectArgsToJsonString: boolean;
|
|
712
|
+
private salvageXmlToolCalls: boolean;
|
|
697
713
|
private omitToolChoiceWhenReasoning: boolean;
|
|
698
714
|
|
|
699
715
|
constructor(
|
|
@@ -722,6 +738,8 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
722
738
|
this.assistantReasoningField = options.assistantReasoningField;
|
|
723
739
|
this.coerceObjectArgsToJsonString =
|
|
724
740
|
options.coerceObjectArgsToJsonString ?? false;
|
|
741
|
+
this.salvageXmlToolCalls =
|
|
742
|
+
options.salvageXmlToolCalls ?? shouldSalvageXmlToolCalls(model);
|
|
725
743
|
this.omitToolChoiceWhenReasoning =
|
|
726
744
|
options.omitToolChoiceWhenReasoning ?? false;
|
|
727
745
|
}
|
|
@@ -740,12 +758,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
740
758
|
const modelOverride = configObj?.model as string | undefined;
|
|
741
759
|
const effort = configObj?.effort as string | undefined;
|
|
742
760
|
const logitBias = configObj?.logit_bias as
|
|
743
|
-
Record<string, number>
|
|
761
|
+
| Record<string, number>
|
|
762
|
+
| undefined;
|
|
744
763
|
const topP = configObj?.top_p as number | undefined;
|
|
745
764
|
const usageAttributionHeaders = configObj?.usageAttributionHeaders as
|
|
746
|
-
Record<string, string>
|
|
765
|
+
| Record<string, string>
|
|
766
|
+
| undefined;
|
|
747
767
|
const perRequestHeaders = configObj?.requestHeaders as
|
|
748
|
-
Record<string, string>
|
|
768
|
+
| Record<string, string>
|
|
769
|
+
| undefined;
|
|
749
770
|
|
|
750
771
|
// Per-tool keys whose object schemas were rewritten to JSON strings for the
|
|
751
772
|
// wire, to be decoded back on the response. Empty unless
|
|
@@ -756,7 +777,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
756
777
|
const openaiMessages = await this.toOpenAIMessages(
|
|
757
778
|
messages,
|
|
758
779
|
systemPrompt,
|
|
759
|
-
|
|
780
|
+
requestSupportsInlineAudio(modelOverride ?? this.model),
|
|
760
781
|
);
|
|
761
782
|
|
|
762
783
|
recordProviderRequestDiagnostics({
|
|
@@ -812,6 +833,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
812
833
|
) as OpenAI.Chat.Completions.ChatCompletionCreateParams["reasoning_effort"];
|
|
813
834
|
}
|
|
814
835
|
|
|
836
|
+
const offeredToolNames = new Set((tools ?? []).map((t) => t.name));
|
|
837
|
+
let xmlToolCallSalvageEnabled = false;
|
|
838
|
+
|
|
815
839
|
if (tools && tools.length > 0) {
|
|
816
840
|
params.tools = tools.map((t) => {
|
|
817
841
|
let parameters = t.input_schema as OpenAI.FunctionParameters;
|
|
@@ -844,6 +868,8 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
844
868
|
// receive them; the generic openai-compatible adapter drops every
|
|
845
869
|
// explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
|
|
846
870
|
const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
|
|
871
|
+
xmlToolCallSalvageEnabled =
|
|
872
|
+
this.salvageXmlToolCalls && toolChoice !== "none";
|
|
847
873
|
if (toolChoice !== undefined) {
|
|
848
874
|
const thinkingOn = isThinkingEnabledOnWire(params);
|
|
849
875
|
const skipAutoDefault = thinkingOn && toolChoice === "auto";
|
|
@@ -862,6 +888,33 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
862
888
|
let reasoningText = "";
|
|
863
889
|
let insideThinkBlock = false;
|
|
864
890
|
let pendingContent = "";
|
|
891
|
+
let xmlHeld = "";
|
|
892
|
+
|
|
893
|
+
const emitVisibleText = (delta: string): void => {
|
|
894
|
+
if (!delta) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (!xmlToolCallSalvageEnabled) {
|
|
898
|
+
contentText += delta;
|
|
899
|
+
onEvent?.({ type: "text_delta", text: delta });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const split = splitXmlToolCallHoldback(xmlHeld + delta);
|
|
903
|
+
xmlHeld = split.held;
|
|
904
|
+
if (split.visible) {
|
|
905
|
+
contentText += split.visible;
|
|
906
|
+
onEvent?.({ type: "text_delta", text: split.visible });
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
const flushHeldXmlAsText = (): void => {
|
|
911
|
+
if (!xmlHeld) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
contentText += xmlHeld;
|
|
915
|
+
onEvent?.({ type: "text_delta", text: xmlHeld });
|
|
916
|
+
xmlHeld = "";
|
|
917
|
+
};
|
|
865
918
|
|
|
866
919
|
const flushPendingContent = (final: boolean): void => {
|
|
867
920
|
while (pendingContent.length > 0) {
|
|
@@ -896,8 +949,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
896
949
|
if (openIdx >= 0) {
|
|
897
950
|
const text = pendingContent.substring(0, openIdx);
|
|
898
951
|
if (text) {
|
|
899
|
-
|
|
900
|
-
onEvent?.({ type: "text_delta", text });
|
|
952
|
+
emitVisibleText(text);
|
|
901
953
|
}
|
|
902
954
|
insideThinkBlock = true;
|
|
903
955
|
pendingContent = pendingContent.substring(
|
|
@@ -909,9 +961,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
909
961
|
: partialTagSuffix(pendingContent, "<think>");
|
|
910
962
|
const safeLen = pendingContent.length - partial;
|
|
911
963
|
if (safeLen > 0) {
|
|
912
|
-
|
|
913
|
-
contentText += t;
|
|
914
|
-
onEvent?.({ type: "text_delta", text: t });
|
|
964
|
+
emitVisibleText(pendingContent.substring(0, safeLen));
|
|
915
965
|
}
|
|
916
966
|
pendingContent =
|
|
917
967
|
partial > 0 ? pendingContent.substring(safeLen) : "";
|
|
@@ -940,6 +990,10 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
940
990
|
...(usageAttributionHeaders ?? {}),
|
|
941
991
|
...(perRequestHeaders ?? {}),
|
|
942
992
|
};
|
|
993
|
+
const extraBody = this.buildRequestExtraBody(options);
|
|
994
|
+
if (extraBody) {
|
|
995
|
+
Object.assign(params, extraBody);
|
|
996
|
+
}
|
|
943
997
|
const createStream = () =>
|
|
944
998
|
this.client.chat.completions.create(params, {
|
|
945
999
|
signal: timeoutSignal,
|
|
@@ -1020,8 +1074,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1020
1074
|
pendingContent += choice.delta.content;
|
|
1021
1075
|
flushPendingContent(false);
|
|
1022
1076
|
} else {
|
|
1023
|
-
|
|
1024
|
-
onEvent?.({ type: "text_delta", text: choice.delta.content });
|
|
1077
|
+
emitVisibleText(choice.delta.content);
|
|
1025
1078
|
}
|
|
1026
1079
|
}
|
|
1027
1080
|
|
|
@@ -1142,6 +1195,42 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1142
1195
|
flushPendingContent(true);
|
|
1143
1196
|
}
|
|
1144
1197
|
|
|
1198
|
+
// DeepSeek (and any caller that sets salvageXmlToolCalls) may emit
|
|
1199
|
+
// `<invoke>` XML as assistant text instead of native `tool_calls`.
|
|
1200
|
+
// Convert complete offered-tool invokes into tool_use blocks so the
|
|
1201
|
+
// agent loop can execute them. Native tool_calls win.
|
|
1202
|
+
if (xmlToolCallSalvageEnabled && toolCallMap.size === 0) {
|
|
1203
|
+
const salvaged = salvageXmlToolCalls(
|
|
1204
|
+
contentText + xmlHeld,
|
|
1205
|
+
offeredToolNames,
|
|
1206
|
+
);
|
|
1207
|
+
if (salvaged) {
|
|
1208
|
+
contentText = salvaged.text;
|
|
1209
|
+
xmlHeld = "";
|
|
1210
|
+
log.info(
|
|
1211
|
+
{
|
|
1212
|
+
salvagedToolCount: salvaged.calls.length,
|
|
1213
|
+
salvagedToolNames: salvaged.calls.map((call) => call.name),
|
|
1214
|
+
},
|
|
1215
|
+
"Converted XML-formatted assistant text into native tool calls",
|
|
1216
|
+
);
|
|
1217
|
+
for (const [index, call] of salvaged.calls.entries()) {
|
|
1218
|
+
toolCallMap.set(index, {
|
|
1219
|
+
id: `call_${randomUUID()}`,
|
|
1220
|
+
name: call.name,
|
|
1221
|
+
args: JSON.stringify(call.input),
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
if (finishReason === "stop" || finishReason === "unknown") {
|
|
1225
|
+
finishReason = "tool_calls";
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
flushHeldXmlAsText();
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
flushHeldXmlAsText();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1145
1234
|
// Build content blocks
|
|
1146
1235
|
const finalReasoning = this.parseThinkTags
|
|
1147
1236
|
? reasoningText.trim()
|
|
@@ -1362,6 +1451,17 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1362
1451
|
return this.extraCreateParams;
|
|
1363
1452
|
}
|
|
1364
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* Fields merged onto the `chat.completions.create` params object. The
|
|
1456
|
+
* OpenAI Node SDK sends unknown body fields as-is, so hosted-Qwen
|
|
1457
|
+
* `directions` reach the runtime proxy on the JSON body.
|
|
1458
|
+
*/
|
|
1459
|
+
protected buildRequestExtraBody(
|
|
1460
|
+
_options?: SendMessageOptions,
|
|
1461
|
+
): Record<string, unknown> | undefined {
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1365
1465
|
/**
|
|
1366
1466
|
* Per-request reasoning_effort ceiling. Defaults to the provider-wide
|
|
1367
1467
|
* `maxReasoningEffort` from constructor options. Subclasses (e.g. Fireworks)
|
|
@@ -47,7 +47,7 @@ export interface OpenAIResponsesProviderOptions {
|
|
|
47
47
|
streamTimeoutMs?: number;
|
|
48
48
|
useNativeWebSearch?: boolean;
|
|
49
49
|
/** When true, target the Codex subscription endpoint and strip fields it
|
|
50
|
-
* rejects (`max_output_tokens`). */
|
|
50
|
+
* rejects (`max_output_tokens`, `prompt_cache_options`, breakpoints). */
|
|
51
51
|
codexSubscription?: boolean;
|
|
52
52
|
/** Static HTTP headers sent with every request (e.g. OpenRouter app
|
|
53
53
|
* attribution). Merged under any per-request attribution headers. */
|
|
@@ -309,13 +309,13 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
309
309
|
|
|
310
310
|
// A per-conversation prompt-cache key gives OpenAI's cache router a
|
|
311
311
|
// stable affinity key so a conversation's requests land on the same
|
|
312
|
-
// cache shard. Every model
|
|
312
|
+
// cache shard. Every Responses model receives it, including the Codex
|
|
313
|
+
// subscription endpoint: the key is a supported routing field there
|
|
314
|
+
// (the official Codex client defaults it from thread identity). Both
|
|
313
315
|
// breakpoint-capable models (which additionally opt into explicit mode
|
|
314
|
-
// below) and implicit-mode models, which carry no explicit breakpoints
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
// there.
|
|
318
|
-
if (!this.codexSubscription && promptCacheKey) {
|
|
316
|
+
// below) and implicit-mode models, which carry no explicit breakpoints,
|
|
317
|
+
// still gain prefix-cache routing affinity from a stable key.
|
|
318
|
+
if (promptCacheKey) {
|
|
319
319
|
params.prompt_cache_key = promptCacheKey;
|
|
320
320
|
}
|
|
321
321
|
|
|
@@ -329,7 +329,9 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
329
329
|
// breakpoints neither uses the cache nor incurs cache-write charges,
|
|
330
330
|
// which is exactly the opt-out `disableCache` wants (omitting the param
|
|
331
331
|
// would re-enable implicit mode). The Codex subscription endpoint
|
|
332
|
-
// rejects
|
|
332
|
+
// rejects `prompt_cache_options` and block-level breakpoints, so those
|
|
333
|
+
// stay off there. Codex runs in implicit prefix-cache mode and relies
|
|
334
|
+
// on `prompt_cache_key` above for routing affinity.
|
|
333
335
|
if (
|
|
334
336
|
!this.codexSubscription &&
|
|
335
337
|
PROMPT_CACHE_BREAKPOINT_MODEL_IDS.has(effectiveModel)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Canonical source for API-key-addressable providers.
|
|
3
3
|
*
|
|
4
4
|
* This module composes the full set of providers that store API keys in
|
|
5
|
-
* secure storage (the `api_key` secret type) from
|
|
5
|
+
* secure storage (the `api_key` secret type) from five sources:
|
|
6
6
|
*
|
|
7
7
|
* 1. **LLM providers** -- derived from `PROVIDER_CATALOG`
|
|
8
8
|
* (`model-catalog.ts`). Adding a provider to the catalog automatically
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 4. **TTS catalog providers** -- dynamically derived from the canonical
|
|
16
16
|
* TTS provider catalog by selecting entries whose secret requirements
|
|
17
17
|
* use the bare-name (non-credential) storage convention.
|
|
18
|
+
* 5. **Embedding-only providers** -- currently `custom`, the
|
|
19
|
+
* OpenAI-compatible memory embeddings endpoint.
|
|
18
20
|
*
|
|
19
21
|
* Consumers that need the set of valid API-key provider names should
|
|
20
22
|
* import {@link API_KEY_PROVIDERS} from this module rather than
|
|
@@ -116,6 +118,13 @@ function catalogApiKeyNames(): string[] {
|
|
|
116
118
|
);
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Embedding-only API-key names. `custom` is the OpenAI-compatible memory
|
|
123
|
+
* embeddings provider and is not an LLM catalog entry, so it is declared
|
|
124
|
+
* here rather than in `PROVIDER_CATALOG`.
|
|
125
|
+
*/
|
|
126
|
+
const EMBEDDING_API_KEY_PROVIDERS: readonly string[] = ["custom"];
|
|
127
|
+
|
|
119
128
|
// ---------------------------------------------------------------------------
|
|
120
129
|
// Unified export
|
|
121
130
|
// ---------------------------------------------------------------------------
|
|
@@ -148,6 +157,7 @@ export const API_KEY_PROVIDERS: readonly string[] = (() => {
|
|
|
148
157
|
...LLM_AND_SEARCH_API_KEY_PROVIDERS,
|
|
149
158
|
...sttApiKeyProviderNames(),
|
|
150
159
|
...catalogApiKeyNames(),
|
|
160
|
+
...EMBEDDING_API_KEY_PROVIDERS,
|
|
151
161
|
]) {
|
|
152
162
|
if (!seen.has(name)) {
|
|
153
163
|
seen.add(name);
|