@stigmer/runner 3.9.0 → 3.11.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/README.md +12 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-llm.js +9 -10
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +2 -1
- package/dist/activities/classify-tool-approvals.js +28 -2
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +32 -0
- package/dist/activities/discover-mcp-server.js +162 -27
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
- package/dist/activities/execute-cursor/approval-state.js +7 -1
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +25 -4
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/env-resolver.js +3 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
- package/dist/activities/execute-cursor/error-classifier.js +81 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
- package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
- package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
- package/dist/activities/execute-cursor/hook-script.js +72 -10
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +5 -1
- package/dist/activities/execute-cursor/index.js +59 -58
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
- package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
- package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
- package/dist/activities/execute-cursor/prompt-builder.js +12 -7
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -1
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/environment.js +3 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +15 -0
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
- package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
- package/dist/activities/execute-deep-agent/setup.js +65 -26
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +3 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/workflow-event-activities.d.ts +28 -10
- package/dist/activities/workflow-event-activities.js +87 -58
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +21 -1
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -4
- package/dist/client/stigmer-client.js +28 -15
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +32 -0
- package/dist/encryption/config.js +68 -0
- package/dist/encryption/config.js.map +1 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.js +3 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/payload-codec.d.ts +41 -0
- package/dist/encryption/payload-codec.js +130 -0
- package/dist/encryption/payload-codec.js.map +1 -0
- package/dist/payload-codecs.d.ts +16 -0
- package/dist/payload-codecs.js +38 -0
- package/dist/payload-codecs.js.map +1 -0
- package/dist/preflight.d.ts +31 -0
- package/dist/preflight.js +43 -0
- package/dist/preflight.js.map +1 -1
- package/dist/runner-manager.js +5 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +5 -16
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +9 -3
- package/dist/shared/approval-policy.js +15 -6
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/attachment-naming.d.ts +53 -0
- package/dist/shared/attachment-naming.js +59 -0
- package/dist/shared/attachment-naming.js.map +1 -0
- package/dist/shared/attachment-vision.d.ts +43 -2
- package/dist/shared/attachment-vision.js +72 -6
- package/dist/shared/attachment-vision.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +23 -2
- package/dist/shared/caller-identity.js +36 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +1 -0
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +42 -1
- package/dist/shared/checkpointer/http-saver.js +96 -8
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-attachment.js +1 -0
- package/dist/shared/conversation-attachment.js.map +1 -1
- package/dist/shared/datastore-attachment.d.ts +50 -7
- package/dist/shared/datastore-attachment.js +93 -11
- package/dist/shared/datastore-attachment.js.map +1 -1
- package/dist/shared/http-retry.d.ts +43 -0
- package/dist/shared/http-retry.js +50 -0
- package/dist/shared/http-retry.js.map +1 -0
- package/dist/shared/llm-backend.d.ts +275 -0
- package/dist/shared/llm-backend.js +425 -0
- package/dist/shared/llm-backend.js.map +1 -0
- package/dist/shared/llm-proxy.d.ts +8 -0
- package/dist/shared/llm-proxy.js +15 -0
- package/dist/shared/llm-proxy.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +57 -0
- package/dist/shared/mcp-enabled-tools.js +86 -0
- package/dist/shared/mcp-enabled-tools.js.map +1 -0
- package/dist/shared/mcp-manager.d.ts +17 -2
- package/dist/shared/mcp-manager.js +37 -25
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +39 -2
- package/dist/shared/mcp-resolver.js +38 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/model-client.d.ts +12 -5
- package/dist/shared/model-client.js +138 -18
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-error.js +198 -5
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/model-registry.d.ts +20 -2
- package/dist/shared/model-registry.js +37 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -0
- package/dist/shared/plan-mode-permissions.js +28 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -4
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +18 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
- package/dist/workflows/call-agent-orchestrator.js +1 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +7 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/engine-core.js +23 -2
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +1 -1
- package/dist/workflows/execute-from-execution.js +11 -1
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/claimcheck-codec.test.ts +36 -0
- package/src/__tests__/encryption-codec.test.ts +234 -0
- package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
- package/src/__tests__/history-encryption-e2e.test.ts +243 -0
- package/src/__tests__/preflight.test.ts +50 -2
- package/src/activities/__tests__/call-llm.test.ts +75 -0
- package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
- package/src/activities/call-llm.ts +9 -16
- package/src/activities/classify-tool-approvals.ts +34 -4
- package/src/activities/discover-mcp-server.ts +190 -32
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
- package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
- package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
- package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
- package/src/activities/execute-cursor/approval-state.ts +30 -1
- package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
- package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
- package/src/activities/execute-cursor/env-resolver.ts +3 -1
- package/src/activities/execute-cursor/error-classifier.ts +91 -4
- package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
- package/src/activities/execute-cursor/hook-script.ts +74 -10
- package/src/activities/execute-cursor/index.ts +63 -72
- package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
- package/src/activities/execute-cursor/prompt-builder.ts +34 -9
- package/src/activities/execute-cursor/turn-stream.ts +5 -2
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
- package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
- package/src/activities/execute-deep-agent/environment.ts +3 -1
- package/src/activities/execute-deep-agent/index.ts +20 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
- package/src/activities/execute-deep-agent/setup.ts +86 -31
- package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
- package/src/activities/hydrate-workflow-execution.ts +3 -1
- package/src/activities/workflow-event-activities.ts +96 -69
- package/src/claimcheck/payload-codec.ts +33 -1
- package/src/client/__tests__/stigmer-client.test.ts +8 -8
- package/src/client/stigmer-client.ts +32 -18
- package/src/encryption/config.ts +91 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/payload-codec.ts +152 -0
- package/src/payload-codecs.ts +56 -0
- package/src/preflight.ts +45 -0
- package/src/runner-manager.ts +6 -24
- package/src/runner.ts +6 -25
- package/src/shared/__tests__/approval-policy.test.ts +82 -39
- package/src/shared/__tests__/attachment-naming.test.ts +159 -0
- package/src/shared/__tests__/attachment-vision.test.ts +97 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
- package/src/shared/__tests__/caller-identity.test.ts +25 -0
- package/src/shared/__tests__/channel-attachment.test.ts +1 -1
- package/src/shared/__tests__/connect-backfill.test.ts +1 -0
- package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
- package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
- package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
- package/src/shared/__tests__/foundry-seam.test.ts +482 -0
- package/src/shared/__tests__/http-retry.test.ts +67 -0
- package/src/shared/__tests__/llm-backend.test.ts +616 -0
- package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
- package/src/shared/__tests__/mcp-manager.test.ts +170 -2
- package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
- package/src/shared/__tests__/model-client.test.ts +154 -0
- package/src/shared/__tests__/model-error.test.ts +289 -1
- package/src/shared/__tests__/model-registry.test.ts +71 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
- package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
- package/src/shared/__tests__/vertex-seam.test.ts +295 -0
- package/src/shared/approval-policy.ts +14 -7
- package/src/shared/attachment-naming.ts +78 -0
- package/src/shared/attachment-vision.ts +92 -9
- package/src/shared/caller-identity.ts +40 -5
- package/src/shared/channel-attachment.ts +1 -0
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
- package/src/shared/checkpointer/http-saver.ts +117 -9
- package/src/shared/conversation-attachment.ts +1 -0
- package/src/shared/datastore-attachment.ts +106 -11
- package/src/shared/http-retry.ts +50 -0
- package/src/shared/llm-backend.ts +544 -0
- package/src/shared/llm-proxy.ts +15 -0
- package/src/shared/mcp-enabled-tools.ts +105 -0
- package/src/shared/mcp-manager.ts +43 -26
- package/src/shared/mcp-resolver.ts +73 -2
- package/src/shared/model-client.ts +161 -19
- package/src/shared/model-error.ts +222 -4
- package/src/shared/model-registry.ts +50 -2
- package/src/shared/plan-mode-permissions.ts +30 -0
- package/src/worker.ts +4 -5
- package/src/workflow-engine/types.ts +18 -0
- package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
- package/src/workflows/call-agent-orchestrator.ts +10 -0
- package/src/workflows/connect-mcp-server.ts +7 -0
- package/src/workflows/engine-core.ts +23 -2
- package/src/workflows/execute-from-execution.ts +12 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* a non-model error.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { describe, it, expect } from "vitest";
|
|
14
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
15
15
|
import {
|
|
16
16
|
PLATFORM_CAPACITY_SENTINEL,
|
|
17
17
|
classifyModelCallError,
|
|
@@ -211,6 +211,294 @@ describe("classifyModelCallError — connection heuristics and no-signal", () =>
|
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
describe("classifyModelCallError — vertex backend", () => {
|
|
215
|
+
// The Vertex arms activate only for direct-mode Anthropic calls under
|
|
216
|
+
// STIGMER_ANTHROPIC_BACKEND=vertex (resolved from env, deployment-static).
|
|
217
|
+
// Every other test in this file runs with the var unset and pins that the
|
|
218
|
+
// public wordings are untouched.
|
|
219
|
+
function stubVertexEnv() {
|
|
220
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "vertex");
|
|
221
|
+
vi.stubEnv("CLOUD_ML_REGION", "asia-south1");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
afterEach(() => {
|
|
225
|
+
vi.unstubAllEnvs();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const vertexCtx = {
|
|
229
|
+
proxyMode: false,
|
|
230
|
+
provider: "anthropic" as const,
|
|
231
|
+
modelId: "claude-sonnet-4-6",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
it("classifies google-auth credential failures as non-retryable with the GCP fix", () => {
|
|
235
|
+
stubVertexEnv();
|
|
236
|
+
for (const raw of [
|
|
237
|
+
"Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.",
|
|
238
|
+
"Unable to detect a Project Id in the current environment.",
|
|
239
|
+
"No projectId was given and it could not be resolved from credentials. The client should be instantiated with the `projectId` option or the `ANTHROPIC_VERTEX_PROJECT_ID` environment variable should be set.",
|
|
240
|
+
"invalid_grant: Invalid JWT Signature.",
|
|
241
|
+
]) {
|
|
242
|
+
const classified = classifyModelCallError(middlewareWrap(new Error(raw)), vertexCtx);
|
|
243
|
+
expect(classified?.code, raw).toBe("LLM_BACKEND_CREDENTIALS");
|
|
244
|
+
expect(classified?.retryable, raw).toBe(false);
|
|
245
|
+
expect(classified?.message, raw).toContain("GOOGLE_APPLICATION_CREDENTIALS");
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does NOT classify credential prose when the backend is not vertex (no relabeling drift)", () => {
|
|
250
|
+
// Same message, backend unset: google-auth prose can only mean vertex —
|
|
251
|
+
// without it, there is no positive model-call signal, so no classification.
|
|
252
|
+
const classified = classifyModelCallError(
|
|
253
|
+
new Error("Could not load the default credentials"),
|
|
254
|
+
vertexCtx,
|
|
255
|
+
);
|
|
256
|
+
expect(classified).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("words 401 around Google credentials, not an API key", () => {
|
|
260
|
+
stubVertexEnv();
|
|
261
|
+
const classified = classifyModelCallError(sdkError(401, "unauthorized"), vertexCtx);
|
|
262
|
+
expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
|
|
263
|
+
expect(classified?.message).toContain("Google rejected this Vertex AI call");
|
|
264
|
+
expect(classified?.message).not.toContain("API key");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("words 403 around the service account's Vertex AI role", () => {
|
|
268
|
+
stubVertexEnv();
|
|
269
|
+
const classified = classifyModelCallError(sdkError(403, "forbidden"), vertexCtx);
|
|
270
|
+
expect(classified?.code).toBe("LLM_PERMISSION_DENIED");
|
|
271
|
+
expect(classified?.message).toContain("Vertex AI User");
|
|
272
|
+
expect(classified?.message).toContain("aiplatform.endpoints.predict");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("words 404 around Model Garden enablement and the configured region", () => {
|
|
276
|
+
stubVertexEnv();
|
|
277
|
+
const classified = classifyModelCallError(sdkError(404, "not found"), vertexCtx);
|
|
278
|
+
expect(classified?.code).toBe("LLM_MODEL_NOT_FOUND");
|
|
279
|
+
expect(classified?.message).toContain("Model Garden");
|
|
280
|
+
expect(classified?.message).toContain('region "asia-south1"');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("stays inert in proxy mode even with the backend var set (proxy owns routing)", () => {
|
|
284
|
+
stubVertexEnv();
|
|
285
|
+
const classified = classifyModelCallError(
|
|
286
|
+
sdkError(401, "unauthorized"),
|
|
287
|
+
{ proxyMode: true, provider: "anthropic" },
|
|
288
|
+
);
|
|
289
|
+
expect(classified?.message).toContain("Stigmer platform");
|
|
290
|
+
expect(classified?.message).not.toContain("Vertex");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("stays inert for OpenAI failures with the Anthropic backend var set", () => {
|
|
294
|
+
stubVertexEnv();
|
|
295
|
+
const classified = classifyModelCallError(
|
|
296
|
+
sdkError(404, "not found"),
|
|
297
|
+
{ proxyMode: false, provider: "openai", modelId: "gpt-4.1" },
|
|
298
|
+
);
|
|
299
|
+
expect(classified?.message).not.toContain("Vertex");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("never throws on an invalid backend value — classification reads it as public", () => {
|
|
303
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "verteks");
|
|
304
|
+
const classified = classifyModelCallError(sdkError(401, "unauthorized"), vertexCtx);
|
|
305
|
+
expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
|
|
306
|
+
expect(classified?.message).toContain("API key");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("classifyModelCallError — bedrock backend", () => {
|
|
311
|
+
// The Bedrock arms activate only for direct-mode Anthropic calls under
|
|
312
|
+
// STIGMER_ANTHROPIC_BACKEND=bedrock, mirroring the vertex suite above.
|
|
313
|
+
function stubBedrockEnv() {
|
|
314
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "bedrock");
|
|
315
|
+
vi.stubEnv("AWS_REGION", "ap-south-1");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
vi.unstubAllEnvs();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const bedrockCtx = {
|
|
323
|
+
proxyMode: false,
|
|
324
|
+
provider: "anthropic" as const,
|
|
325
|
+
modelId: "claude-sonnet-4-6",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
it("classifies AWS credential-chain failures as non-retryable with the AWS fix", () => {
|
|
329
|
+
stubBedrockEnv();
|
|
330
|
+
for (const raw of [
|
|
331
|
+
"Could not load credentials from any providers",
|
|
332
|
+
"Resolved credential object is not valid",
|
|
333
|
+
]) {
|
|
334
|
+
const classified = classifyModelCallError(middlewareWrap(new Error(raw)), bedrockCtx);
|
|
335
|
+
expect(classified?.code, raw).toBe("LLM_BACKEND_CREDENTIALS");
|
|
336
|
+
expect(classified?.retryable, raw).toBe(false);
|
|
337
|
+
expect(classified?.message, raw).toContain("AWS");
|
|
338
|
+
expect(classified?.message, raw).toContain("AWS_BEARER_TOKEN_BEDROCK");
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("does NOT classify credential prose when the backend is not bedrock (no relabeling drift)", () => {
|
|
343
|
+
const classified = classifyModelCallError(
|
|
344
|
+
new Error("Could not load credentials from any providers"),
|
|
345
|
+
bedrockCtx,
|
|
346
|
+
);
|
|
347
|
+
expect(classified).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("translates the bare-id inference-profile rejection into the one-var remedy", () => {
|
|
351
|
+
stubBedrockEnv();
|
|
352
|
+
// AWS's actual ValidationException prose for newer Claude models
|
|
353
|
+
// invoked by bare model id.
|
|
354
|
+
const classified = classifyModelCallError(
|
|
355
|
+
sdkError(
|
|
356
|
+
400,
|
|
357
|
+
"Invocation of model ID anthropic.claude-sonnet-4-6-v1:0 with on-demand throughput isn't supported. Retry your request with the ID or ARN of an inference profile that contains this model.",
|
|
358
|
+
),
|
|
359
|
+
bedrockCtx,
|
|
360
|
+
);
|
|
361
|
+
expect(classified?.code).toBe("LLM_BACKEND_MODEL_ROUTING");
|
|
362
|
+
expect(classified?.retryable).toBe(false);
|
|
363
|
+
expect(classified?.message).toContain("STIGMER_BEDROCK_INFERENCE_PREFIX");
|
|
364
|
+
expect(classified?.message).toContain("STIGMER_BEDROCK_MODEL_MAP");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("words 401 around AWS identity, not an API key", () => {
|
|
368
|
+
stubBedrockEnv();
|
|
369
|
+
const classified = classifyModelCallError(sdkError(401, "unauthorized"), bedrockCtx);
|
|
370
|
+
expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
|
|
371
|
+
expect(classified?.message).toContain("AWS rejected this Bedrock call");
|
|
372
|
+
expect(classified?.message).not.toContain("API key");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("words 403 around Bedrock model access and IAM, not an API key", () => {
|
|
376
|
+
stubBedrockEnv();
|
|
377
|
+
const classified = classifyModelCallError(sdkError(403, "forbidden"), bedrockCtx);
|
|
378
|
+
expect(classified?.code).toBe("LLM_PERMISSION_DENIED");
|
|
379
|
+
expect(classified?.message).toContain("Model access");
|
|
380
|
+
expect(classified?.message).toContain("bedrock:InvokeModel");
|
|
381
|
+
expect(classified?.message).not.toContain("API key");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("words 404 around region availability and the id-resolution knobs", () => {
|
|
385
|
+
stubBedrockEnv();
|
|
386
|
+
const classified = classifyModelCallError(sdkError(404, "not found"), bedrockCtx);
|
|
387
|
+
expect(classified?.code).toBe("LLM_MODEL_NOT_FOUND");
|
|
388
|
+
expect(classified?.message).toContain('region "ap-south-1"');
|
|
389
|
+
expect(classified?.message).toContain("STIGMER_BEDROCK_MODEL_MAP");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("stays inert in proxy mode even with the backend var set (proxy owns routing)", () => {
|
|
393
|
+
stubBedrockEnv();
|
|
394
|
+
const classified = classifyModelCallError(
|
|
395
|
+
sdkError(401, "unauthorized"),
|
|
396
|
+
{ proxyMode: true, provider: "anthropic" },
|
|
397
|
+
);
|
|
398
|
+
expect(classified?.message).toContain("Stigmer platform");
|
|
399
|
+
expect(classified?.message).not.toContain("Bedrock");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("keeps vertex wordings and bedrock wordings from cross-contaminating", () => {
|
|
403
|
+
stubBedrockEnv();
|
|
404
|
+
const classified = classifyModelCallError(sdkError(403, "forbidden"), bedrockCtx);
|
|
405
|
+
expect(classified?.message).not.toContain("Vertex");
|
|
406
|
+
expect(classified?.message).not.toContain("Model Garden");
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("classifyModelCallError — foundry backend", () => {
|
|
411
|
+
// The Foundry arms activate only for direct-mode Anthropic calls under
|
|
412
|
+
// STIGMER_ANTHROPIC_BACKEND=foundry, mirroring the vertex and bedrock
|
|
413
|
+
// suites above.
|
|
414
|
+
function stubFoundryEnv() {
|
|
415
|
+
vi.stubEnv("STIGMER_ANTHROPIC_BACKEND", "foundry");
|
|
416
|
+
vi.stubEnv("ANTHROPIC_FOUNDRY_RESOURCE", "my-foundry-resource");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
afterEach(() => {
|
|
420
|
+
vi.unstubAllEnvs();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const foundryCtx = {
|
|
424
|
+
proxyMode: false,
|
|
425
|
+
provider: "anthropic" as const,
|
|
426
|
+
modelId: "claude-sonnet-4-6",
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
it("classifies Entra token-acquisition failures as non-retryable with the Azure fix", () => {
|
|
430
|
+
stubFoundryEnv();
|
|
431
|
+
// The Foundry SDK wraps every token-provider failure in this one
|
|
432
|
+
// prefix (pinned by foundry-seam.test.ts), so a single wording covers
|
|
433
|
+
// the whole @azure/identity credential-chain family.
|
|
434
|
+
const classified = classifyModelCallError(
|
|
435
|
+
middlewareWrap(
|
|
436
|
+
new Error(
|
|
437
|
+
"Failed to get token from azureADTokenProvider: " +
|
|
438
|
+
"ChainedTokenCredential authentication failed. CredentialUnavailableError: " +
|
|
439
|
+
"ManagedIdentityCredential: no managed identity endpoint found.",
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
foundryCtx,
|
|
443
|
+
);
|
|
444
|
+
expect(classified?.code).toBe("LLM_BACKEND_CREDENTIALS");
|
|
445
|
+
expect(classified?.retryable).toBe(false);
|
|
446
|
+
expect(classified?.message).toContain("Microsoft Entra ID");
|
|
447
|
+
expect(classified?.message).toContain("ANTHROPIC_FOUNDRY_API_KEY");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("does NOT classify credential prose when the backend is not foundry (no relabeling drift)", () => {
|
|
451
|
+
const classified = classifyModelCallError(
|
|
452
|
+
new Error("Failed to get token from azureADTokenProvider: boom"),
|
|
453
|
+
foundryCtx,
|
|
454
|
+
);
|
|
455
|
+
expect(classified).toBeUndefined();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("words 401 around the Foundry credential, not the Anthropic API key", () => {
|
|
459
|
+
stubFoundryEnv();
|
|
460
|
+
const classified = classifyModelCallError(sdkError(401, "unauthorized"), foundryCtx);
|
|
461
|
+
expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
|
|
462
|
+
expect(classified?.message).toContain("Azure rejected this Microsoft Foundry call");
|
|
463
|
+
expect(classified?.message).toContain("ANTHROPIC_FOUNDRY_API_KEY");
|
|
464
|
+
expect(classified?.message).not.toContain("your API key");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("words 403 around the Foundry RBAC role", () => {
|
|
468
|
+
stubFoundryEnv();
|
|
469
|
+
const classified = classifyModelCallError(sdkError(403, "forbidden"), foundryCtx);
|
|
470
|
+
expect(classified?.code).toBe("LLM_PERMISSION_DENIED");
|
|
471
|
+
expect(classified?.message).toContain("Foundry User");
|
|
472
|
+
expect(classified?.message).not.toContain("API key");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("words 404 around deployment names, the resource, and the deployment map", () => {
|
|
476
|
+
stubFoundryEnv();
|
|
477
|
+
const classified = classifyModelCallError(sdkError(404, "not found"), foundryCtx);
|
|
478
|
+
expect(classified?.code).toBe("LLM_MODEL_NOT_FOUND");
|
|
479
|
+
expect(classified?.message).toContain("deployment name");
|
|
480
|
+
expect(classified?.message).toContain('resource "my-foundry-resource"');
|
|
481
|
+
expect(classified?.message).toContain("STIGMER_FOUNDRY_DEPLOYMENT_MAP");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("stays inert in proxy mode even with the backend var set (proxy owns routing)", () => {
|
|
485
|
+
stubFoundryEnv();
|
|
486
|
+
const classified = classifyModelCallError(
|
|
487
|
+
sdkError(401, "unauthorized"),
|
|
488
|
+
{ proxyMode: true, provider: "anthropic" },
|
|
489
|
+
);
|
|
490
|
+
expect(classified?.message).toContain("Stigmer platform");
|
|
491
|
+
expect(classified?.message).not.toContain("Foundry");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("keeps foundry wordings from cross-contaminating with the other backends", () => {
|
|
495
|
+
stubFoundryEnv();
|
|
496
|
+
const classified = classifyModelCallError(sdkError(404, "not found"), foundryCtx);
|
|
497
|
+
expect(classified?.message).not.toContain("Vertex");
|
|
498
|
+
expect(classified?.message).not.toContain("Bedrock");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
214
502
|
describe("describeExecutionError", () => {
|
|
215
503
|
it("labels classified model errors with the stable code, not the wrapper class", () => {
|
|
216
504
|
const { errorType, errorMessage } = describeExecutionError(
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
getSummarizationModel,
|
|
4
4
|
getEconomyModel,
|
|
5
5
|
getDefaultModel,
|
|
6
|
+
getModelVisionCapability,
|
|
6
7
|
resolveToApiModelId,
|
|
7
8
|
_resetRegistryCache,
|
|
8
9
|
} from "../model-registry.js";
|
|
@@ -253,6 +254,75 @@ describe("resolveToApiModelId", () => {
|
|
|
253
254
|
});
|
|
254
255
|
});
|
|
255
256
|
|
|
257
|
+
describe("getModelVisionCapability", () => {
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
_resetRegistryCache();
|
|
260
|
+
vi.restoreAllMocks();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
afterEach(() => {
|
|
264
|
+
_resetRegistryCache();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("returns the explicit vision flag when the capabilities block is present", async () => {
|
|
268
|
+
mockRegistryResponse([
|
|
269
|
+
{ id: "claude-sonnet-4.6", provider: "anthropic", costTier: "standard", harness: "native", capabilities: { vision: true, toolUse: true } },
|
|
270
|
+
{ id: "llama3.1", provider: "ollama", costTier: "economy", harness: "native", capabilities: { vision: false, toolUse: true } },
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
expect(await getModelVisionCapability("claude-sonnet-4.6")).toBe(true);
|
|
274
|
+
_resetRegistryCache();
|
|
275
|
+
mockRegistryResponse([
|
|
276
|
+
{ id: "llama3.1", provider: "ollama", costTier: "economy", harness: "native", capabilities: { vision: false } },
|
|
277
|
+
]);
|
|
278
|
+
expect(await getModelVisionCapability("llama3.1")).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("returns undefined when the capabilities block is absent (never assessed ≠ blind)", async () => {
|
|
282
|
+
// The cursor-harness convention today: pricing + UI fields, no
|
|
283
|
+
// capabilities block. Must stay undefined, never coerce to false.
|
|
284
|
+
mockRegistryResponse([
|
|
285
|
+
{ id: "composer-2.5", provider: "cursor", costTier: "economy", harness: "cursor" },
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
expect(await getModelVisionCapability("composer-2.5")).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("matches by apiModelId as well as registry id", async () => {
|
|
292
|
+
// getDefaultModel() hands the deep-agent harness the provider API id, so
|
|
293
|
+
// both identifier forms must resolve to the same capability.
|
|
294
|
+
mockRegistryResponse([
|
|
295
|
+
{ id: "claude-sonnet-4.6", apiModelId: "claude-sonnet-4-6", provider: "anthropic", costTier: "standard", harness: "native", capabilities: { vision: true } },
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
expect(await getModelVisionCapability("claude-sonnet-4-6")).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns undefined for unknown names, the empty string, and the Auto pool", async () => {
|
|
302
|
+
mockRegistryResponse([
|
|
303
|
+
{ id: "claude-sonnet-4.6", provider: "anthropic", costTier: "standard", harness: "native", capabilities: { vision: true } },
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
expect(await getModelVisionCapability("never-heard-of-it")).toBeUndefined();
|
|
307
|
+
expect(await getModelVisionCapability("")).toBeUndefined();
|
|
308
|
+
expect(await getModelVisionCapability("default")).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("returns undefined when the registry fetch fails (fail-open)", async () => {
|
|
312
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("network error"));
|
|
313
|
+
|
|
314
|
+
expect(await getModelVisionCapability("claude-sonnet-4.6")).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("treats a malformed capabilities value as undefined", async () => {
|
|
318
|
+
mockRegistryResponse([
|
|
319
|
+
{ id: "weird-model", provider: "anthropic", costTier: "standard", harness: "native", capabilities: "yes" as unknown as { vision?: boolean } },
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
expect(await getModelVisionCapability("weird-model")).toBeUndefined();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
256
326
|
interface MockModel {
|
|
257
327
|
id: string;
|
|
258
328
|
apiModelId?: string;
|
|
@@ -260,6 +330,7 @@ interface MockModel {
|
|
|
260
330
|
costTier: string;
|
|
261
331
|
harness: string;
|
|
262
332
|
featured?: boolean;
|
|
333
|
+
capabilities?: { vision?: boolean; toolUse?: boolean };
|
|
263
334
|
}
|
|
264
335
|
|
|
265
336
|
function mockRegistryResponse(models: MockModel[]) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter test for the vertex backend branch of buildChatModel — the
|
|
3
|
+
* complement of vertex-seam.test.ts.
|
|
4
|
+
*
|
|
5
|
+
* The seam test pins the REAL AnthropicVertex client's wire behavior under
|
|
6
|
+
* a hand-built ChatAnthropic. This file pins OUR production wiring: a real
|
|
7
|
+
* `buildChatModel` and a real `ChatAnthropic` drive a mocked
|
|
8
|
+
* `@anthropic-ai/vertex-sdk`, so LangChain's lazy `createClient` invocation
|
|
9
|
+
* path (client constructed on first request, once per cached client) is
|
|
10
|
+
* exercised exactly as production does it. Together the two files cover the
|
|
11
|
+
* whole chain without any test-only injection seam in production code.
|
|
12
|
+
*
|
|
13
|
+
* Pinned here:
|
|
14
|
+
* - the factory forwards LangChain's `maxRetries: 0` to the client (a
|
|
15
|
+
* factory that drops it nests SDK retries inside LangChain's own loop),
|
|
16
|
+
* - the factory forwards the request timeout (clientOptions.timeout ->
|
|
17
|
+
* factory options -> SDK constructor; how STIGMER_LLM_REQUEST_TIMEOUT_MS
|
|
18
|
+
* bounds this backend),
|
|
19
|
+
* - construction and invocation succeed with NO ANTHROPIC_API_KEY (Vertex
|
|
20
|
+
* auth is Google's, and ChatAnthropic waives the key when createClient
|
|
21
|
+
* is provided),
|
|
22
|
+
* - the translated `@date` id is what crosses the wire while the returned
|
|
23
|
+
* apiModelId stays canonical (the canonical-id invariant),
|
|
24
|
+
* - usage_metadata survives the adapter (what billing reads).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
28
|
+
import { HumanMessage, AIMessage } from "@langchain/core/messages";
|
|
29
|
+
|
|
30
|
+
const { vertexCtorArgs, createRequests } = vi.hoisted(() => ({
|
|
31
|
+
vertexCtorArgs: [] as Array<{ maxRetries?: number; timeout?: number }>,
|
|
32
|
+
createRequests: [] as Array<Record<string, unknown>>,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@anthropic-ai/vertex-sdk", () => ({
|
|
36
|
+
AnthropicVertex: class {
|
|
37
|
+
readonly maxRetries: number | undefined;
|
|
38
|
+
readonly messages = {
|
|
39
|
+
create: async (request: Record<string, unknown>) => {
|
|
40
|
+
createRequests.push(request);
|
|
41
|
+
return {
|
|
42
|
+
id: "msg_vertex_adapter_01",
|
|
43
|
+
type: "message",
|
|
44
|
+
role: "assistant",
|
|
45
|
+
model: request.model,
|
|
46
|
+
content: [{ type: "text", text: "Namaste from Vertex." }],
|
|
47
|
+
stop_reason: "end_turn",
|
|
48
|
+
stop_sequence: null,
|
|
49
|
+
usage: { input_tokens: 7, output_tokens: 5 },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
constructor(opts: { maxRetries?: number; timeout?: number }) {
|
|
55
|
+
vertexCtorArgs.push(opts);
|
|
56
|
+
this.maxRetries = opts.maxRetries;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
62
|
+
|
|
63
|
+
import { buildChatModel } from "../model-client.js";
|
|
64
|
+
import { _resetRegistryCache } from "../model-registry.js";
|
|
65
|
+
import { toVertexModelId } from "../llm-backend.js";
|
|
66
|
+
|
|
67
|
+
describe("buildChatModel vertex adapter", () => {
|
|
68
|
+
const savedEnv = { ...process.env };
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
_resetRegistryCache();
|
|
72
|
+
vertexCtorArgs.length = 0;
|
|
73
|
+
createRequests.length = 0;
|
|
74
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
75
|
+
process.env.STIGMER_ANTHROPIC_BACKEND = "vertex";
|
|
76
|
+
process.env.CLOUD_ML_REGION = "asia-south1";
|
|
77
|
+
// Registry offline → resolveToApiModelId passes the id through, keeping
|
|
78
|
+
// this test free of registry fixtures (that path has its own tests).
|
|
79
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry offline"));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
_resetRegistryCache();
|
|
84
|
+
vi.restoreAllMocks();
|
|
85
|
+
process.env = { ...savedEnv };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("drives the mocked client through the real createClient path with the invariants intact", async () => {
|
|
89
|
+
const { model, apiModelId } = await buildChatModel({
|
|
90
|
+
modelName: "claude-sonnet-4-5-20250929",
|
|
91
|
+
maxTokens: 1024,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Canonical id returned; the client is not constructed yet (lazy factory).
|
|
95
|
+
expect(apiModelId).toBe("claude-sonnet-4-5-20250929");
|
|
96
|
+
expect(vertexCtorArgs).toHaveLength(0);
|
|
97
|
+
|
|
98
|
+
const result = await model.invoke([new HumanMessage("Weather in Chennai?")]);
|
|
99
|
+
|
|
100
|
+
// First request constructed exactly one (batch) client, with LangChain's
|
|
101
|
+
// maxRetries: 0 honored — LangChain owns retrying.
|
|
102
|
+
expect(vertexCtorArgs).toHaveLength(1);
|
|
103
|
+
expect(vertexCtorArgs[0].maxRetries).toBe(0);
|
|
104
|
+
|
|
105
|
+
// The wire carries the Vertex-translated id; billing already got the
|
|
106
|
+
// canonical one above.
|
|
107
|
+
expect(createRequests).toHaveLength(1);
|
|
108
|
+
expect(createRequests[0].model).toBe("claude-sonnet-4-5@20250929");
|
|
109
|
+
expect(createRequests[0].max_tokens).toBe(1024);
|
|
110
|
+
|
|
111
|
+
// Response and usage flow back through LangChain untouched.
|
|
112
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
113
|
+
expect(result.text).toBe("Namaste from Vertex.");
|
|
114
|
+
expect((result as AIMessage).usage_metadata).toMatchObject({
|
|
115
|
+
input_tokens: 7,
|
|
116
|
+
output_tokens: 5,
|
|
117
|
+
total_tokens: 12,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("translated ids inherit the same default maxTokens as canonical ids", () => {
|
|
122
|
+
// setup.ts deliberately omits maxTokens, so the LangChain per-model
|
|
123
|
+
// default applies. That default prefix-matches the model string — and the
|
|
124
|
+
// vertex branch hands ChatAnthropic the TRANSLATED string. This pins the
|
|
125
|
+
// equality the zero-behavior-change criterion rests on (verified 16384 ==
|
|
126
|
+
// 16384 for 4.5-generation ids at @langchain/anthropic 1.4.0); a future
|
|
127
|
+
// bump that diverges the two forms fails here instead of silently capping
|
|
128
|
+
// vertex deployments differently from public ones.
|
|
129
|
+
for (const canonical of [
|
|
130
|
+
"claude-sonnet-4-5-20250929",
|
|
131
|
+
"claude-haiku-4-5-20251001",
|
|
132
|
+
"claude-sonnet-4-6",
|
|
133
|
+
]) {
|
|
134
|
+
const publicModel = new ChatAnthropic({ model: canonical, apiKey: "probe" });
|
|
135
|
+
const vertexModel = new ChatAnthropic({ model: toVertexModelId(canonical), apiKey: "probe" });
|
|
136
|
+
expect(vertexModel.maxTokens, canonical).toBe(publicModel.maxTokens);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("forwards the request timeout to the SDK client (STIGMER_LLM_REQUEST_TIMEOUT_MS path)", async () => {
|
|
141
|
+
// The timeout rides clientOptions -> factory options -> SDK constructor.
|
|
142
|
+
// A factory that drops it silently unbounds the operator's timeout on
|
|
143
|
+
// this backend (the regression T06 repaired for the public path).
|
|
144
|
+
const { model } = await buildChatModel({
|
|
145
|
+
modelName: "claude-sonnet-4-6",
|
|
146
|
+
maxTokens: 256,
|
|
147
|
+
timeoutMs: 5000,
|
|
148
|
+
});
|
|
149
|
+
await model.invoke([new HumanMessage("hi")]);
|
|
150
|
+
|
|
151
|
+
expect(vertexCtorArgs).toHaveLength(1);
|
|
152
|
+
expect(vertexCtorArgs[0].timeout).toBe(5000);
|
|
153
|
+
expect(vertexCtorArgs[0].maxRetries).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("constructs and invokes with no ANTHROPIC_API_KEY anywhere in the environment", async () => {
|
|
157
|
+
expect(process.env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
158
|
+
|
|
159
|
+
const { model } = await buildChatModel({
|
|
160
|
+
modelName: "claude-sonnet-4-6",
|
|
161
|
+
maxTokens: 256,
|
|
162
|
+
});
|
|
163
|
+
const result = await model.invoke([new HumanMessage("hi")]);
|
|
164
|
+
|
|
165
|
+
expect(result).toBeInstanceOf(AIMessage);
|
|
166
|
+
// Dateless 4.6-generation id crossed the wire untranslated.
|
|
167
|
+
expect(createRequests.at(-1)?.model).toBe("claude-sonnet-4-6");
|
|
168
|
+
});
|
|
169
|
+
});
|