@vellumai/assistant 0.10.2-dev.202606242023.5e459ba → 0.10.2-dev.202606242135.63f618e
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/openapi.yaml +48 -0
- package/package.json +1 -1
- package/src/__tests__/config-loader-backfill.test.ts +108 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
- package/src/__tests__/openai-provider.test.ts +22 -12
- package/src/__tests__/openai-responses-provider.test.ts +12 -2
- package/src/__tests__/provider-error-scenarios.test.ts +5 -4
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
- package/src/api/responses/llm-request-log-entry.ts +4 -0
- package/src/config/loader.ts +2 -0
- package/src/config/prune-seeded-callsite-defaults.ts +110 -0
- package/src/daemon/mcp-reload-service.ts +10 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
- package/src/mcp/client.ts +15 -1
- package/src/mcp/mcp-auth-orchestrator.ts +6 -1
- package/src/mcp/mcp-header-store.ts +134 -0
- package/src/memory/llm-request-log-store.ts +26 -1
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
- package/src/providers/openai/api-error-normalization.ts +270 -0
- package/src/providers/openai/chat-completions-provider.ts +37 -83
- package/src/providers/openai/responses-provider.ts +45 -46
- package/src/runtime/routes/llm-context-normalization.ts +12 -0
- package/src/runtime/routes/mcp-auth-routes.ts +78 -11
- package/src/util/errors.ts +26 -1
- package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
package/openapi.yaml
CHANGED
|
@@ -8408,6 +8408,22 @@ paths:
|
|
|
8408
8408
|
anyOf:
|
|
8409
8409
|
- type: number
|
|
8410
8410
|
- type: "null"
|
|
8411
|
+
apiErrorCode:
|
|
8412
|
+
anyOf:
|
|
8413
|
+
- type: string
|
|
8414
|
+
- type: "null"
|
|
8415
|
+
apiErrorType:
|
|
8416
|
+
anyOf:
|
|
8417
|
+
- type: string
|
|
8418
|
+
- type: "null"
|
|
8419
|
+
apiErrorParam:
|
|
8420
|
+
anyOf:
|
|
8421
|
+
- type: string
|
|
8422
|
+
- type: "null"
|
|
8423
|
+
requestId:
|
|
8424
|
+
anyOf:
|
|
8425
|
+
- type: string
|
|
8426
|
+
- type: "null"
|
|
8411
8427
|
additionalProperties: false
|
|
8412
8428
|
- type: "null"
|
|
8413
8429
|
required:
|
|
@@ -15352,6 +15368,22 @@ paths:
|
|
|
15352
15368
|
anyOf:
|
|
15353
15369
|
- type: number
|
|
15354
15370
|
- type: "null"
|
|
15371
|
+
apiErrorCode:
|
|
15372
|
+
anyOf:
|
|
15373
|
+
- type: string
|
|
15374
|
+
- type: "null"
|
|
15375
|
+
apiErrorType:
|
|
15376
|
+
anyOf:
|
|
15377
|
+
- type: string
|
|
15378
|
+
- type: "null"
|
|
15379
|
+
apiErrorParam:
|
|
15380
|
+
anyOf:
|
|
15381
|
+
- type: string
|
|
15382
|
+
- type: "null"
|
|
15383
|
+
requestId:
|
|
15384
|
+
anyOf:
|
|
15385
|
+
- type: string
|
|
15386
|
+
- type: "null"
|
|
15355
15387
|
additionalProperties: false
|
|
15356
15388
|
- type: "null"
|
|
15357
15389
|
required:
|
|
@@ -17809,6 +17841,22 @@ paths:
|
|
|
17809
17841
|
anyOf:
|
|
17810
17842
|
- type: number
|
|
17811
17843
|
- type: "null"
|
|
17844
|
+
apiErrorCode:
|
|
17845
|
+
anyOf:
|
|
17846
|
+
- type: string
|
|
17847
|
+
- type: "null"
|
|
17848
|
+
apiErrorType:
|
|
17849
|
+
anyOf:
|
|
17850
|
+
- type: string
|
|
17851
|
+
- type: "null"
|
|
17852
|
+
apiErrorParam:
|
|
17853
|
+
anyOf:
|
|
17854
|
+
- type: string
|
|
17855
|
+
- type: "null"
|
|
17856
|
+
requestId:
|
|
17857
|
+
anyOf:
|
|
17858
|
+
- type: string
|
|
17859
|
+
- type: "null"
|
|
17812
17860
|
additionalProperties: false
|
|
17813
17861
|
- type: "null"
|
|
17814
17862
|
required:
|
package/package.json
CHANGED
|
@@ -98,6 +98,64 @@ function writeConfig(obj: unknown): void {
|
|
|
98
98
|
writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function latencySeed(): Record<string, unknown> {
|
|
102
|
+
return {
|
|
103
|
+
model: "claude-haiku-4-5-20251001",
|
|
104
|
+
effort: "low",
|
|
105
|
+
thinking: { enabled: false },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fullSeededCallSites(): Record<string, Record<string, unknown>> {
|
|
110
|
+
return {
|
|
111
|
+
guardianQuestionCopy: latencySeed(),
|
|
112
|
+
interactionClassifier: latencySeed(),
|
|
113
|
+
skillCategoryInference: latencySeed(),
|
|
114
|
+
inviteInstructionGenerator: latencySeed(),
|
|
115
|
+
notificationDecision: latencySeed(),
|
|
116
|
+
preferenceExtraction: latencySeed(),
|
|
117
|
+
commitMessage: {
|
|
118
|
+
model: "claude-haiku-4-5-20251001",
|
|
119
|
+
maxTokens: 120,
|
|
120
|
+
temperature: 0.2,
|
|
121
|
+
effort: "low",
|
|
122
|
+
thinking: { enabled: false },
|
|
123
|
+
},
|
|
124
|
+
conversationStarters: latencySeed(),
|
|
125
|
+
conversationSummarization: {
|
|
126
|
+
model: "claude-opus-4-7",
|
|
127
|
+
effort: "low",
|
|
128
|
+
thinking: { enabled: false },
|
|
129
|
+
},
|
|
130
|
+
recall: {
|
|
131
|
+
profile: "cost-optimized",
|
|
132
|
+
maxTokens: 4096,
|
|
133
|
+
effort: "low",
|
|
134
|
+
thinking: { enabled: false, streamThinking: false },
|
|
135
|
+
temperature: 0,
|
|
136
|
+
disableCache: true,
|
|
137
|
+
},
|
|
138
|
+
heartbeatAgent: {
|
|
139
|
+
profile: "cost-optimized",
|
|
140
|
+
maxTokens: 2048,
|
|
141
|
+
effort: "low",
|
|
142
|
+
temperature: 0,
|
|
143
|
+
thinking: { enabled: false, streamThinking: false },
|
|
144
|
+
contextWindow: { maxInputTokens: 16000 },
|
|
145
|
+
},
|
|
146
|
+
replySuggestion: {
|
|
147
|
+
model: "claude-haiku-4-5-20251001",
|
|
148
|
+
effort: "low",
|
|
149
|
+
thinking: { enabled: false },
|
|
150
|
+
disableCache: true,
|
|
151
|
+
},
|
|
152
|
+
memoryRouter: {
|
|
153
|
+
profile: "cost-optimized",
|
|
154
|
+
contextWindow: { maxInputTokens: 1_000_000 },
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
101
159
|
function mergeDefaultConfigAndSeedInferenceProfiles(db?: DrizzleDb): void {
|
|
102
160
|
const defaultConfigMerge = mergeDefaultWorkspaceConfig();
|
|
103
161
|
seedInferenceProfiles({
|
|
@@ -372,6 +430,56 @@ describe("loadConfig startup behavior", () => {
|
|
|
372
430
|
expect(config.memory.v2.bm25_b).toBe(0.4);
|
|
373
431
|
});
|
|
374
432
|
|
|
433
|
+
test("default workspace config merge prunes exact seeded call-site defaults", () => {
|
|
434
|
+
const seededCallSites = fullSeededCallSites();
|
|
435
|
+
const overlayPath = join(WORKSPACE_DIR, "hatch-overlay.json");
|
|
436
|
+
writeFileSync(
|
|
437
|
+
overlayPath,
|
|
438
|
+
JSON.stringify(
|
|
439
|
+
{
|
|
440
|
+
gateway: {
|
|
441
|
+
unmappedPolicy: "default",
|
|
442
|
+
defaultAssistantId: "self",
|
|
443
|
+
},
|
|
444
|
+
llm: {
|
|
445
|
+
activeProfile: "balanced",
|
|
446
|
+
advisorProfile: "frontier",
|
|
447
|
+
callSites: {
|
|
448
|
+
...seededCallSites,
|
|
449
|
+
recall: {
|
|
450
|
+
...seededCallSites.recall,
|
|
451
|
+
disableCache: false,
|
|
452
|
+
},
|
|
453
|
+
customSite: { profile: "frontier" },
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
null,
|
|
458
|
+
2,
|
|
459
|
+
) + "\n",
|
|
460
|
+
);
|
|
461
|
+
process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH = overlayPath;
|
|
462
|
+
|
|
463
|
+
const result = mergeDefaultWorkspaceConfig();
|
|
464
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as Record<
|
|
465
|
+
string,
|
|
466
|
+
Record<string, unknown>
|
|
467
|
+
>;
|
|
468
|
+
const llm = raw.llm as Record<string, unknown>;
|
|
469
|
+
const callSites = llm.callSites as Record<string, Record<string, unknown>>;
|
|
470
|
+
|
|
471
|
+
expect(result.hadOverlay).toBe(true);
|
|
472
|
+
expect(raw.gateway).toEqual({
|
|
473
|
+
unmappedPolicy: "default",
|
|
474
|
+
defaultAssistantId: "self",
|
|
475
|
+
});
|
|
476
|
+
expect(llm.activeProfile).toBe("balanced");
|
|
477
|
+
expect(llm.advisorProfile).toBe("frontier");
|
|
478
|
+
expect(Object.keys(callSites).sort()).toEqual(["customSite", "recall"]);
|
|
479
|
+
expect(callSites.recall?.disableCache).toBe(false);
|
|
480
|
+
expect(callSites.customSite).toEqual({ profile: "frontier" });
|
|
481
|
+
});
|
|
482
|
+
|
|
375
483
|
test("reloads cached config when config.json is updated externally", () => {
|
|
376
484
|
// Models a CLI subprocess writing twilio.accountSid while the assistant
|
|
377
485
|
// process already has an effective config cached in memory.
|
|
@@ -182,6 +182,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
182
182
|
"daemon/handlers/config-slack-channel.ts", // Slack channel config credential management
|
|
183
183
|
"providers/platform-proxy/context.ts", // managed proxy API key lookup for provider initialization
|
|
184
184
|
"platform/client.ts", // platform client credential store fallback for standalone CLI auth
|
|
185
|
+
"mcp/mcp-header-store.ts", // MCP static auth header persistence (credential store CRUD + legacy migration)
|
|
185
186
|
"mcp/mcp-oauth-provider.ts", // MCP OAuth token/client/discovery persistence
|
|
186
187
|
"runtime/routes/integrations/slack/token.ts", // shared Slack token resolver (bot/user token lookup for CLI use routes)
|
|
187
188
|
"mcp/client.ts", // MCP client cached-token lookup
|
|
@@ -25,18 +25,15 @@
|
|
|
25
25
|
import { describe, expect, test } from "bun:test";
|
|
26
26
|
|
|
27
27
|
import { buildProviderErrorResponsePayload } from "../memory/llm-request-log-store.js";
|
|
28
|
-
import {
|
|
29
|
-
AssistantError,
|
|
30
|
-
ErrorCode,
|
|
31
|
-
ProviderError,
|
|
32
|
-
} from "../util/errors.js";
|
|
28
|
+
import { AssistantError, ErrorCode, ProviderError } from "../util/errors.js";
|
|
33
29
|
|
|
34
|
-
function persisted(err: Error): {
|
|
30
|
+
function persisted(err: Error): {
|
|
31
|
+
error: Record<string, unknown>;
|
|
32
|
+
rawResponse?: unknown;
|
|
33
|
+
} {
|
|
35
34
|
// Round-trip through JSON to assert on the actual stored shape, not the
|
|
36
35
|
// in-memory object reference.
|
|
37
|
-
return JSON.parse(
|
|
38
|
-
JSON.stringify(buildProviderErrorResponsePayload(err)),
|
|
39
|
-
);
|
|
36
|
+
return JSON.parse(JSON.stringify(buildProviderErrorResponsePayload(err)));
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
describe("buildProviderErrorResponsePayload", () => {
|
|
@@ -60,6 +57,34 @@ describe("buildProviderErrorResponsePayload", () => {
|
|
|
60
57
|
});
|
|
61
58
|
});
|
|
62
59
|
|
|
60
|
+
test("ProviderError serializes upstream provider error metadata when present", () => {
|
|
61
|
+
const err = new ProviderError(
|
|
62
|
+
"OpenAI API error (401): Invalid API key provided",
|
|
63
|
+
"openai",
|
|
64
|
+
401,
|
|
65
|
+
{
|
|
66
|
+
apiErrorCode: "invalid_api_key",
|
|
67
|
+
apiErrorType: "invalid_request_error",
|
|
68
|
+
apiErrorParam: "api_key",
|
|
69
|
+
requestId: "req_abc123",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
const got = persisted(err);
|
|
73
|
+
expect(got).toEqual({
|
|
74
|
+
error: {
|
|
75
|
+
name: "ProviderError",
|
|
76
|
+
message: "OpenAI API error (401): Invalid API key provided",
|
|
77
|
+
code: ErrorCode.PROVIDER_ERROR,
|
|
78
|
+
provider: "openai",
|
|
79
|
+
statusCode: 401,
|
|
80
|
+
apiErrorCode: "invalid_api_key",
|
|
81
|
+
apiErrorType: "invalid_request_error",
|
|
82
|
+
apiErrorParam: "api_key",
|
|
83
|
+
requestId: "req_abc123",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
63
88
|
test("ProviderError without optional metadata omits statusCode + retryAfterMs", () => {
|
|
64
89
|
const err = new ProviderError(
|
|
65
90
|
"Gemini API error: surprise internal state",
|
|
@@ -127,6 +152,43 @@ describe("buildProviderErrorResponsePayload", () => {
|
|
|
127
152
|
});
|
|
128
153
|
});
|
|
129
154
|
|
|
155
|
+
test("captured JSON rawBody is attached as a parsed rawResponse sibling", () => {
|
|
156
|
+
// So the inspector's Raw tab can render the actual upstream provider JSON
|
|
157
|
+
// (like a successful row) instead of only the extracted error fields.
|
|
158
|
+
const err = new ProviderError(
|
|
159
|
+
"Together AI API error (400): Model 'MiniMax-M3' is not supported.",
|
|
160
|
+
"together",
|
|
161
|
+
400,
|
|
162
|
+
{
|
|
163
|
+
apiErrorCode: "model_not_supported",
|
|
164
|
+
rawBody: JSON.stringify({
|
|
165
|
+
detail: "Model 'MiniMax-M3' is not supported.",
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
const got = persisted(err);
|
|
170
|
+
expect(got.error.apiErrorCode).toBe("model_not_supported");
|
|
171
|
+
expect(got.rawResponse).toEqual({
|
|
172
|
+
detail: "Model 'MiniMax-M3' is not supported.",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("non-JSON rawBody (HTML/text error page) is kept verbatim as a string", () => {
|
|
177
|
+
const err = new ProviderError("Bad gateway", "openai", 400, {
|
|
178
|
+
rawBody: "<html><body>upstream timeout</body></html>",
|
|
179
|
+
});
|
|
180
|
+
const got = persisted(err);
|
|
181
|
+
expect(got.rawResponse).toBe("<html><body>upstream timeout</body></html>");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("no captured rawBody omits the rawResponse sibling entirely", () => {
|
|
185
|
+
const err = new ProviderError("rate limited", "anthropic", 429, {
|
|
186
|
+
retryAfterMs: 1500,
|
|
187
|
+
});
|
|
188
|
+
const got = persisted(err);
|
|
189
|
+
expect("rawResponse" in got).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
130
192
|
test("ProviderError with statusCode 0 is still recorded (not coerced to undefined)", () => {
|
|
131
193
|
// Defensive: `if (err.statusCode !== undefined)` correctly admits 0.
|
|
132
194
|
// A raw `if (err.statusCode)` would drop it, so the test guards against
|
|
@@ -58,6 +58,16 @@ let lastConstructorOptions: Record<string, unknown> | null = null;
|
|
|
58
58
|
let shouldThrow: Error | null = null;
|
|
59
59
|
const DEFAULT_SDK_TIMEOUT_MS = 1_860_000;
|
|
60
60
|
|
|
61
|
+
// Each provider installs a `fetch` wrapper to capture raw error bodies, so the
|
|
62
|
+
// constructor options carry a `fetch` function; assert the meaningful options
|
|
63
|
+
// via objectContaining and confirm `fetch` is wired.
|
|
64
|
+
function expectOpenAIConstructorOptions(
|
|
65
|
+
expected: Record<string, unknown>,
|
|
66
|
+
): void {
|
|
67
|
+
expect(lastConstructorOptions).toEqual(expect.objectContaining(expected));
|
|
68
|
+
expect(typeof lastConstructorOptions?.fetch).toBe("function");
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
function userMsg(text: string): Message {
|
|
62
72
|
return { role: "user", content: [{ type: "text", text }] };
|
|
63
73
|
}
|
|
@@ -309,7 +319,7 @@ describe("OpenAIProvider", () => {
|
|
|
309
319
|
});
|
|
310
320
|
|
|
311
321
|
expect(compatible.name).toBe("ollama");
|
|
312
|
-
|
|
322
|
+
expectOpenAIConstructorOptions({
|
|
313
323
|
apiKey: "sk-local",
|
|
314
324
|
baseURL: "http://127.0.0.1:11434/v1",
|
|
315
325
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -322,7 +332,7 @@ describe("OpenAIProvider", () => {
|
|
|
322
332
|
delete process.env.OLLAMA_BASE_URL;
|
|
323
333
|
const ollama = new OllamaProvider("llama3.2");
|
|
324
334
|
expect(ollama.name).toBe("ollama");
|
|
325
|
-
|
|
335
|
+
expectOpenAIConstructorOptions({
|
|
326
336
|
apiKey: "ollama",
|
|
327
337
|
baseURL: "http://127.0.0.1:11434/v1",
|
|
328
338
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -342,7 +352,7 @@ describe("OpenAIProvider", () => {
|
|
|
342
352
|
process.env.OLLAMA_BASE_URL = " ";
|
|
343
353
|
const ollama = new OllamaProvider("llama3.2");
|
|
344
354
|
expect(ollama.name).toBe("ollama");
|
|
345
|
-
|
|
355
|
+
expectOpenAIConstructorOptions({
|
|
346
356
|
apiKey: "ollama",
|
|
347
357
|
baseURL: "http://127.0.0.1:11434/v1",
|
|
348
358
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1260,7 +1270,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1260
1270
|
});
|
|
1261
1271
|
|
|
1262
1272
|
expect(managed.name).toBe("openai");
|
|
1263
|
-
|
|
1273
|
+
expectOpenAIConstructorOptions({
|
|
1264
1274
|
apiKey: "ast-key-123",
|
|
1265
1275
|
baseURL: "https://platform.example.com/v1/runtime-proxy/openai",
|
|
1266
1276
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1270,7 +1280,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1270
1280
|
test("OpenAIProvider without baseURL calls provider directly", () => {
|
|
1271
1281
|
new OpenAIProvider("sk-user-key", "gpt-4o");
|
|
1272
1282
|
|
|
1273
|
-
|
|
1283
|
+
expectOpenAIConstructorOptions({
|
|
1274
1284
|
apiKey: "sk-user-key",
|
|
1275
1285
|
baseURL: undefined,
|
|
1276
1286
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1282,7 +1292,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1282
1292
|
streamTimeoutMs: 300_000,
|
|
1283
1293
|
});
|
|
1284
1294
|
|
|
1285
|
-
|
|
1295
|
+
expectOpenAIConstructorOptions({
|
|
1286
1296
|
apiKey: "sk-user-key",
|
|
1287
1297
|
baseURL: undefined,
|
|
1288
1298
|
timeout: 360_000,
|
|
@@ -1299,7 +1309,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1299
1309
|
);
|
|
1300
1310
|
|
|
1301
1311
|
expect(managed.name).toBe("fireworks");
|
|
1302
|
-
|
|
1312
|
+
expectOpenAIConstructorOptions({
|
|
1303
1313
|
apiKey: "ast-key-123",
|
|
1304
1314
|
baseURL: "https://platform.example.com/v1/runtime-proxy/fireworks",
|
|
1305
1315
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1312,7 +1322,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1312
1322
|
"accounts/fireworks/models/llama-v3p1-70b-instruct",
|
|
1313
1323
|
);
|
|
1314
1324
|
|
|
1315
|
-
|
|
1325
|
+
expectOpenAIConstructorOptions({
|
|
1316
1326
|
apiKey: "fw-user-key",
|
|
1317
1327
|
baseURL: "https://api.fireworks.ai/inference/v1",
|
|
1318
1328
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1325,7 +1335,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1325
1335
|
});
|
|
1326
1336
|
|
|
1327
1337
|
expect(managed.name).toBe("openrouter");
|
|
1328
|
-
|
|
1338
|
+
expectOpenAIConstructorOptions({
|
|
1329
1339
|
apiKey: "ast-key-123",
|
|
1330
1340
|
baseURL: "https://platform.example.com/v1/runtime-proxy/openrouter",
|
|
1331
1341
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1335,7 +1345,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1335
1345
|
test("OpenRouterProvider without custom baseURL uses default OpenRouter URL", () => {
|
|
1336
1346
|
new OpenRouterProvider("or-user-key", "openai/gpt-4o");
|
|
1337
1347
|
|
|
1338
|
-
|
|
1348
|
+
expectOpenAIConstructorOptions({
|
|
1339
1349
|
apiKey: "or-user-key",
|
|
1340
1350
|
baseURL: "https://openrouter.ai/api/v1",
|
|
1341
1351
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1348,7 +1358,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1348
1358
|
});
|
|
1349
1359
|
|
|
1350
1360
|
expect(managed.name).toBe("minimax");
|
|
1351
|
-
|
|
1361
|
+
expectOpenAIConstructorOptions({
|
|
1352
1362
|
apiKey: "ast-key-123",
|
|
1353
1363
|
baseURL: "https://platform.example.com/v1/runtime-proxy/minimax",
|
|
1354
1364
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -1358,7 +1368,7 @@ describe("custom baseURL initialization", () => {
|
|
|
1358
1368
|
test("MinimaxProvider without custom baseURL uses default MiniMax URL", () => {
|
|
1359
1369
|
new MinimaxProvider("mm-user-key", "MiniMax-M2.7");
|
|
1360
1370
|
|
|
1361
|
-
|
|
1371
|
+
expectOpenAIConstructorOptions({
|
|
1362
1372
|
apiKey: "mm-user-key",
|
|
1363
1373
|
baseURL: "https://api.minimax.io/v1",
|
|
1364
1374
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -24,6 +24,16 @@ let lastConstructorOptions: Record<string, unknown> | null = null;
|
|
|
24
24
|
let shouldThrow: Error | null = null;
|
|
25
25
|
const DEFAULT_SDK_TIMEOUT_MS = 1_860_000;
|
|
26
26
|
|
|
27
|
+
// Each provider installs a `fetch` wrapper to capture raw error bodies, so the
|
|
28
|
+
// constructor options carry a `fetch` function; assert the meaningful options
|
|
29
|
+
// via objectContaining and confirm `fetch` is wired.
|
|
30
|
+
function expectOpenAIConstructorOptions(
|
|
31
|
+
expected: Record<string, unknown>,
|
|
32
|
+
): void {
|
|
33
|
+
expect(lastConstructorOptions).toEqual(expect.objectContaining(expected));
|
|
34
|
+
expect(typeof lastConstructorOptions?.fetch).toBe("function");
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// Simulate OpenAI.APIError
|
|
28
38
|
class FakeAPIError extends Error {
|
|
29
39
|
status: number;
|
|
@@ -218,7 +228,7 @@ describe("OpenAIResponsesProvider", () => {
|
|
|
218
228
|
providerLabel: "Managed OpenAI",
|
|
219
229
|
});
|
|
220
230
|
|
|
221
|
-
|
|
231
|
+
expectOpenAIConstructorOptions({
|
|
222
232
|
apiKey: "sk-custom",
|
|
223
233
|
baseURL: "https://proxy.example.com/v1",
|
|
224
234
|
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
@@ -230,7 +240,7 @@ describe("OpenAIResponsesProvider", () => {
|
|
|
230
240
|
streamTimeoutMs: 300_000,
|
|
231
241
|
});
|
|
232
242
|
|
|
233
|
-
|
|
243
|
+
expectOpenAIConstructorOptions({
|
|
234
244
|
apiKey: "sk-custom",
|
|
235
245
|
baseURL: undefined,
|
|
236
246
|
timeout: 360_000,
|
|
@@ -837,9 +837,10 @@ describe("RetryProvider — streaming response handling", () => {
|
|
|
837
837
|
|
|
838
838
|
test("does NOT retry OpenAI/Gemini-shaped 'Request was aborted' (no inner-timeout rewrite at those catch-sites)", async () => {
|
|
839
839
|
// The OpenAI chat-completions, OpenAI responses, and Gemini catch-sites
|
|
840
|
-
// format their errors as `"<Provider> API error (
|
|
841
|
-
// was aborted."` (
|
|
842
|
-
// Anthropic catch-site intentionally omits
|
|
840
|
+
// format their errors as `"<Provider> API error (<status>): Request
|
|
841
|
+
// was aborted."` (the OpenAI catch-sites render a missing status as
|
|
842
|
+
// `(unknown status)`; the Anthropic catch-site intentionally omits the
|
|
843
|
+
// parenthetical) and — unlike the Anthropic
|
|
843
844
|
// catch-site — they do NOT rewrite their inner-streamTimeoutMs
|
|
844
845
|
// deadline failures. A provider-agnostic transport-abort predicate
|
|
845
846
|
// would burn three retries on what is by construction a deterministic
|
|
@@ -848,7 +849,7 @@ describe("RetryProvider — streaming response handling", () => {
|
|
|
848
849
|
// wasted retry budget for non-Anthropic providers until their
|
|
849
850
|
// catch-sites grow the same `innerTimeoutFired` distinction.
|
|
850
851
|
const openaiAbortError = new ProviderError(
|
|
851
|
-
"OpenAI API error (
|
|
852
|
+
"OpenAI API error (unknown status): Request was aborted.",
|
|
852
853
|
"openai",
|
|
853
854
|
undefined,
|
|
854
855
|
);
|