@vellumai/assistant 0.11.5 → 0.11.6-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 +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -405,7 +405,7 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
405
405
|
expect(assistantMsg.reasoning_content).toBeUndefined();
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
test("backfills placeholder content for a reasoning-only assistant turn
|
|
408
|
+
test("backfills placeholder content for a reasoning-only assistant turn", async () => {
|
|
409
409
|
const { provider, requests } = stubProvider(
|
|
410
410
|
[
|
|
411
411
|
{
|
|
@@ -413,10 +413,7 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
413
413
|
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
414
414
|
},
|
|
415
415
|
],
|
|
416
|
-
{
|
|
417
|
-
assistantReasoningField: "reasoning",
|
|
418
|
-
backfillEmptyAssistantContent: true,
|
|
419
|
-
},
|
|
416
|
+
{ assistantReasoningField: "reasoning" },
|
|
420
417
|
);
|
|
421
418
|
|
|
422
419
|
await provider.sendMessage([
|
|
@@ -455,53 +452,38 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
455
452
|
expect(EMPTY_ASSISTANT_TURN_PLACEHOLDER).not.toContain("\x00");
|
|
456
453
|
});
|
|
457
454
|
|
|
458
|
-
test("
|
|
459
|
-
const { provider, requests } = stubProvider(
|
|
460
|
-
|
|
461
|
-
{
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
],
|
|
466
|
-
{ assistantReasoningField: "reasoning_content" },
|
|
467
|
-
);
|
|
455
|
+
test("backfills placeholder for a whitespace-only assistant turn", async () => {
|
|
456
|
+
const { provider, requests } = stubProvider([
|
|
457
|
+
{
|
|
458
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
459
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
460
|
+
},
|
|
461
|
+
]);
|
|
468
462
|
|
|
469
463
|
await provider.sendMessage([
|
|
470
464
|
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
471
|
-
{
|
|
472
|
-
role: "assistant",
|
|
473
|
-
content: [
|
|
474
|
-
{
|
|
475
|
-
type: "thinking",
|
|
476
|
-
thinking: "truncated chain of thought",
|
|
477
|
-
signature: "",
|
|
478
|
-
},
|
|
479
|
-
],
|
|
480
|
-
},
|
|
465
|
+
{ role: "assistant", content: [{ type: "text", text: " \n" }] },
|
|
481
466
|
]);
|
|
482
467
|
|
|
483
468
|
const params = requests[0] as {
|
|
484
469
|
messages: Array<{ role: string; content: string | null }>;
|
|
485
470
|
};
|
|
486
471
|
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
expect(assistantMsg.content).
|
|
472
|
+
// Validators that trim before checking presence treat whitespace-only
|
|
473
|
+
// content as absent, so it needs the same placeholder as empty content.
|
|
474
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
490
475
|
});
|
|
491
476
|
|
|
492
477
|
test("backfills placeholder when thinking is dropped and no text was emitted", async () => {
|
|
493
478
|
// Custom openai-compatible endpoints do not set assistantReasoningField, so
|
|
494
|
-
// a Stop during thinking serializes to
|
|
479
|
+
// a Stop during thinking serializes to blank content with no tool calls
|
|
495
480
|
// unless the backfill guard runs.
|
|
496
|
-
const { provider, requests } = stubProvider(
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
],
|
|
503
|
-
{ backfillEmptyAssistantContent: true },
|
|
504
|
-
);
|
|
481
|
+
const { provider, requests } = stubProvider([
|
|
482
|
+
{
|
|
483
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
484
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
485
|
+
},
|
|
486
|
+
]);
|
|
505
487
|
|
|
506
488
|
await provider.sendMessage([
|
|
507
489
|
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
@@ -534,15 +516,12 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
534
516
|
});
|
|
535
517
|
|
|
536
518
|
test("backfills placeholder for an empty aborted assistant turn", async () => {
|
|
537
|
-
const { provider, requests } = stubProvider(
|
|
538
|
-
|
|
539
|
-
{
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
],
|
|
544
|
-
{ backfillEmptyAssistantContent: true },
|
|
545
|
-
);
|
|
519
|
+
const { provider, requests } = stubProvider([
|
|
520
|
+
{
|
|
521
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
522
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
523
|
+
},
|
|
524
|
+
]);
|
|
546
525
|
|
|
547
526
|
await provider.sendMessage([
|
|
548
527
|
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
@@ -557,15 +536,12 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
557
536
|
});
|
|
558
537
|
|
|
559
538
|
test("does not backfill content when tool calls are present", async () => {
|
|
560
|
-
const { provider, requests } = stubProvider(
|
|
561
|
-
|
|
562
|
-
{
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
],
|
|
567
|
-
{ backfillEmptyAssistantContent: true },
|
|
568
|
-
);
|
|
539
|
+
const { provider, requests } = stubProvider([
|
|
540
|
+
{
|
|
541
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
542
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
543
|
+
},
|
|
544
|
+
]);
|
|
569
545
|
|
|
570
546
|
await provider.sendMessage([
|
|
571
547
|
{
|
|
@@ -992,7 +968,9 @@ describe("thinking-mode tool_choice rejection fallback", () => {
|
|
|
992
968
|
|
|
993
969
|
expect(requests).toHaveLength(2);
|
|
994
970
|
expect((requests[0] as { tool_choice?: string }).tool_choice).toBe("none");
|
|
995
|
-
expect(
|
|
971
|
+
expect(
|
|
972
|
+
(requests[1] as { tool_choice?: string }).tool_choice,
|
|
973
|
+
).toBeUndefined();
|
|
996
974
|
});
|
|
997
975
|
|
|
998
976
|
test("does not retry a 4xx that does not name tool_choice", async () => {
|
|
@@ -1103,9 +1081,13 @@ describe("missing reasoning_content rejection fallback", () => {
|
|
|
1103
1081
|
);
|
|
1104
1082
|
expect(/reasoning_content/i.test(wrapped.message)).toBe(false);
|
|
1105
1083
|
|
|
1106
|
-
const { provider, requests } = stubProviderWithErrors(
|
|
1107
|
-
|
|
1108
|
-
|
|
1084
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1085
|
+
[wrapped],
|
|
1086
|
+
OK_CHUNKS,
|
|
1087
|
+
{
|
|
1088
|
+
assistantReasoningField: "reasoning_content",
|
|
1089
|
+
},
|
|
1090
|
+
);
|
|
1109
1091
|
|
|
1110
1092
|
await provider.sendMessage(toolCallHistory);
|
|
1111
1093
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
|
|
5
|
+
import { ProviderError } from "../../../util/errors.js";
|
|
6
|
+
import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
|
|
7
|
+
|
|
8
|
+
function providerThatThrows(error: unknown): OpenAIChatCompletionsProvider {
|
|
9
|
+
const provider = new OpenAIChatCompletionsProvider("test-key", "test-model", {
|
|
10
|
+
baseURL: "http://127.0.0.1:1/v1",
|
|
11
|
+
providerName: "openai-compatible",
|
|
12
|
+
providerLabel: "OpenAI-compatible",
|
|
13
|
+
});
|
|
14
|
+
(provider as unknown as { client: unknown }).client = {
|
|
15
|
+
chat: {
|
|
16
|
+
completions: {
|
|
17
|
+
create: async () => {
|
|
18
|
+
throw error;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return provider;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("connection-error wrapping", () => {
|
|
27
|
+
test("wraps APIConnectionError with reason network_error and the original as cause", async () => {
|
|
28
|
+
const cause = Object.assign(new Error("connect ECONNREFUSED"), {
|
|
29
|
+
code: "ECONNREFUSED",
|
|
30
|
+
});
|
|
31
|
+
const sdkError = new OpenAI.APIConnectionError({ cause });
|
|
32
|
+
const thrown = await providerThatThrows(sdkError)
|
|
33
|
+
.sendMessage([{ role: "user", content: [{ type: "text", text: "hi" }] }])
|
|
34
|
+
.then(
|
|
35
|
+
() => null,
|
|
36
|
+
(e: unknown) => e,
|
|
37
|
+
);
|
|
38
|
+
expect(thrown).toBeInstanceOf(ProviderError);
|
|
39
|
+
const providerError = thrown as ProviderError;
|
|
40
|
+
expect(providerError.reason).toBe("network_error");
|
|
41
|
+
expect(providerError.statusCode).toBeUndefined();
|
|
42
|
+
expect(providerError.cause).toBe(sdkError);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -145,7 +145,7 @@ const ID_SHAPES = [
|
|
|
145
145
|
] as const;
|
|
146
146
|
|
|
147
147
|
/** History whose tool_result is paired with a preceding tool_use. */
|
|
148
|
-
function pairedHistory(id: string): Message[] {
|
|
148
|
+
function pairedHistory(id: string, result = "file contents"): Message[] {
|
|
149
149
|
return [
|
|
150
150
|
{ role: "user", content: [{ type: "text", text: "Read the file" }] },
|
|
151
151
|
{
|
|
@@ -165,7 +165,7 @@ function pairedHistory(id: string): Message[] {
|
|
|
165
165
|
{
|
|
166
166
|
type: "tool_result",
|
|
167
167
|
tool_use_id: id,
|
|
168
|
-
content:
|
|
168
|
+
content: result,
|
|
169
169
|
},
|
|
170
170
|
],
|
|
171
171
|
},
|
|
@@ -383,6 +383,38 @@ describe("OpenAIChatCompletionsProvider orphan tool_result guard", () => {
|
|
|
383
383
|
},
|
|
384
384
|
]);
|
|
385
385
|
});
|
|
386
|
+
|
|
387
|
+
test("keeps JSON Schema references opaque for Gemini-compatible gateways", async () => {
|
|
388
|
+
const provider = makeProvider();
|
|
389
|
+
const stub = stubChatCreate(provider);
|
|
390
|
+
const schemaResult = JSON.stringify({
|
|
391
|
+
$defs: { LLMProvider: { type: "string" } },
|
|
392
|
+
properties: {
|
|
393
|
+
provider: { $ref: "#/$defs/LLMProvider" },
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await provider.sendMessage(pairedHistory("call_schema", schemaResult));
|
|
398
|
+
|
|
399
|
+
const sent = stub.messages();
|
|
400
|
+
const toolMessage = sent.find((message) => message.role === "tool");
|
|
401
|
+
expect(toolMessage).toBeDefined();
|
|
402
|
+
expect(JSON.parse(toolMessage!.content as string)).toEqual({
|
|
403
|
+
output: schemaResult,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("leaves ordinary JSON tool results unchanged", async () => {
|
|
408
|
+
const provider = makeProvider();
|
|
409
|
+
const stub = stubChatCreate(provider);
|
|
410
|
+
const ordinaryResult = JSON.stringify({ status: "ok", count: 3 });
|
|
411
|
+
|
|
412
|
+
await provider.sendMessage(pairedHistory("call_json", ordinaryResult));
|
|
413
|
+
|
|
414
|
+
const sent = stub.messages();
|
|
415
|
+
const toolMessage = sent.find((message) => message.role === "tool");
|
|
416
|
+
expect(toolMessage?.content).toBe(ordinaryResult);
|
|
417
|
+
});
|
|
386
418
|
});
|
|
387
419
|
|
|
388
420
|
// ---------------------------------------------------------------------------
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import OpenAI from "openai";
|
|
2
2
|
|
|
3
3
|
import type { ProviderErrorReason } from "../../util/errors.js";
|
|
4
4
|
import {
|
|
@@ -238,7 +238,21 @@ export function normalizeOpenAIAPIError(
|
|
|
238
238
|
if (rawBody) {
|
|
239
239
|
out.rawBody = rawBody;
|
|
240
240
|
}
|
|
241
|
-
|
|
241
|
+
// Transport failures never reached the server, so there is no status or
|
|
242
|
+
// body for deriveReason to read; the SDK's error class is the only signal.
|
|
243
|
+
// APIUserAbortError (caller cancellation, inner stream deadline) is a
|
|
244
|
+
// sibling class, not a subclass, so it deliberately does NOT match here:
|
|
245
|
+
// classifying it as a transient network error would make the retry loop
|
|
246
|
+
// re-run 30-minute deadline failures. Guarded because test doubles of the
|
|
247
|
+
// openai module may not define the class.
|
|
248
|
+
const connectionErrorClass = OpenAI.APIConnectionError as
|
|
249
|
+
| typeof OpenAI.APIConnectionError
|
|
250
|
+
| undefined;
|
|
251
|
+
out.reason =
|
|
252
|
+
typeof connectionErrorClass === "function" &&
|
|
253
|
+
error instanceof connectionErrorClass
|
|
254
|
+
? "network_error"
|
|
255
|
+
: deriveReason(out, error.status);
|
|
242
256
|
return out;
|
|
243
257
|
}
|
|
244
258
|
|
|
@@ -121,14 +121,15 @@ export function detectVisionNotSupported(
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Fallback `content` for an assistant turn that has neither visible text nor
|
|
124
|
-
* tool calls (e.g. a reasoning-only turn truncated at the output-token limit
|
|
124
|
+
* tool calls (e.g. a reasoning-only turn truncated at the output-token limit,
|
|
125
|
+
* or a Stop mid-stream before any text).
|
|
125
126
|
*
|
|
126
127
|
* The OpenAI chat-completions schema requires an assistant message to carry
|
|
127
128
|
* `content` or `tool_calls`. OpenAI itself tolerates `content: null`/`""` here,
|
|
128
|
-
* but strict OpenAI-compatible backends do not: DeepSeek
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
129
|
+
* but strict OpenAI-compatible backends do not: DeepSeek rejects the request
|
|
130
|
+
* with `Invalid assistant message: content or tool_calls must be set`, and
|
|
131
|
+
* vLLM-style validators coerce empty-string content back to null and reject it
|
|
132
|
+
* the same way. The placeholder must therefore be a non-empty string.
|
|
132
133
|
*
|
|
133
134
|
* We reuse the shared empty-turn sentinel so that
|
|
134
135
|
* `isPlaceholderSentinelText`/`cleanAssistantContent` strip it from persisted
|
|
@@ -168,14 +169,6 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
168
169
|
* tool-call turns. DeepSeek thinking mode that requires the field even when
|
|
169
170
|
* empty is handled by a one-shot retry. */
|
|
170
171
|
assistantReasoningField?: "reasoning" | "reasoning_content";
|
|
171
|
-
/** Backfill a non-empty placeholder for assistant turns that would otherwise
|
|
172
|
-
* serialize with neither `content` nor `tool_calls` (e.g. reasoning-only
|
|
173
|
-
* turns, or a Stop mid-stream before any text). Off by default; enabled for
|
|
174
|
-
* OpenRouter, Vercel AI Gateway, LiteLLM, and custom `openai-compatible`
|
|
175
|
-
* endpoints, whose downstream providers (e.g. DeepSeek, vLLM, Portkey)
|
|
176
|
-
* reject such messages with `Invalid assistant message: content or
|
|
177
|
-
* tool_calls must be set`. See {@link EMPTY_ASSISTANT_TURN_PLACEHOLDER}. */
|
|
178
|
-
backfillEmptyAssistantContent?: boolean;
|
|
179
172
|
/** Present object-typed tool params to the model as JSON-string params and
|
|
180
173
|
* decode them back to objects on the response. Works around models whose
|
|
181
174
|
* function-call serialization collapses nested objects to `{}` (observed
|
|
@@ -248,6 +241,60 @@ function isClientErrorStatus(error: unknown): boolean {
|
|
|
248
241
|
return typeof status === "number" && status >= 400 && status < 500;
|
|
249
242
|
}
|
|
250
243
|
|
|
244
|
+
/**
|
|
245
|
+
* True when a parsed tool result contains a local JSON Schema reference.
|
|
246
|
+
*
|
|
247
|
+
* Gemini reserves `$ref` fields inside structured function responses for
|
|
248
|
+
* multimodal part references. OpenAI-compatible gateways can JSON-decode tool
|
|
249
|
+
* message content before translating it to Gemini, which makes a JSON Schema
|
|
250
|
+
* result look like one of those references and causes an INVALID_ARGUMENT.
|
|
251
|
+
*/
|
|
252
|
+
function containsLocalJsonSchemaReference(value: unknown): boolean {
|
|
253
|
+
const pending: unknown[] = [value];
|
|
254
|
+
while (pending.length > 0) {
|
|
255
|
+
const current = pending.pop();
|
|
256
|
+
if (Array.isArray(current)) {
|
|
257
|
+
pending.push(...current);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (current === null || typeof current !== "object") {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const record = current as Record<string, unknown>;
|
|
265
|
+
const ref = record.$ref;
|
|
266
|
+
if (typeof ref === "string" && (ref === "#" || ref.startsWith("#/"))) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
pending.push(...Object.values(record));
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Keep JSON Schema tool results opaque across OpenAI-compatible gateways.
|
|
276
|
+
*
|
|
277
|
+
* The outer object is intentionally valid JSON so gateways that decode tool
|
|
278
|
+
* content produce `{ output: string }`; the schema's `$ref` remains inside the
|
|
279
|
+
* string and cannot be interpreted as a Gemini multimodal part reference.
|
|
280
|
+
*/
|
|
281
|
+
function protectJsonSchemaToolResult(payload: string): string {
|
|
282
|
+
if (!payload.includes('"$ref"')) {
|
|
283
|
+
return payload;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let parsed: unknown;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(payload);
|
|
289
|
+
} catch {
|
|
290
|
+
return payload;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return containsLocalJsonSchemaReference(parsed)
|
|
294
|
+
? JSON.stringify({ output: payload })
|
|
295
|
+
: payload;
|
|
296
|
+
}
|
|
297
|
+
|
|
251
298
|
/**
|
|
252
299
|
* True when the request carried an explicit reasoning opt-out (`"none"` sent
|
|
253
300
|
* as flat `reasoning_effort` or nested `reasoning.effort`) and the provider
|
|
@@ -556,7 +603,6 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
556
603
|
| "reasoning"
|
|
557
604
|
| "reasoning_content"
|
|
558
605
|
| undefined;
|
|
559
|
-
private backfillEmptyAssistantContent: boolean;
|
|
560
606
|
private coerceObjectArgsToJsonString: boolean;
|
|
561
607
|
private omitToolChoiceWhenReasoning: boolean;
|
|
562
608
|
|
|
@@ -584,8 +630,6 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
584
630
|
this.requestHeaders = options.requestHeaders ?? {};
|
|
585
631
|
this.parseThinkTags = options.parseThinkTags ?? false;
|
|
586
632
|
this.assistantReasoningField = options.assistantReasoningField;
|
|
587
|
-
this.backfillEmptyAssistantContent =
|
|
588
|
-
options.backfillEmptyAssistantContent ?? false;
|
|
589
633
|
this.coerceObjectArgsToJsonString =
|
|
590
634
|
options.coerceObjectArgsToJsonString ?? false;
|
|
591
635
|
this.omitToolChoiceWhenReasoning =
|
|
@@ -709,8 +753,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
709
753
|
if (toolChoice !== undefined) {
|
|
710
754
|
const thinkingOn = isThinkingEnabledOnWire(params);
|
|
711
755
|
const skipAutoDefault = thinkingOn && toolChoice === "auto";
|
|
712
|
-
const skipAllChoices =
|
|
713
|
-
thinkingOn && this.omitToolChoiceWhenReasoning;
|
|
756
|
+
const skipAllChoices = thinkingOn && this.omitToolChoiceWhenReasoning;
|
|
714
757
|
if (!skipAutoDefault && !skipAllChoices) {
|
|
715
758
|
params.tool_choice = toolChoice;
|
|
716
759
|
}
|
|
@@ -1147,7 +1190,10 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1147
1190
|
);
|
|
1148
1191
|
}
|
|
1149
1192
|
const retryAfterMs = extractRetryAfterMs(error.headers);
|
|
1193
|
+
// `cause` keeps the SDK error's errno-bearing chain reachable for
|
|
1194
|
+
// network-shape classification (`isRetryableNetworkError` walks it).
|
|
1150
1195
|
const errorOptions: {
|
|
1196
|
+
cause?: unknown;
|
|
1151
1197
|
retryAfterMs?: number;
|
|
1152
1198
|
abortReason?: unknown;
|
|
1153
1199
|
apiErrorCode?: string;
|
|
@@ -1156,7 +1202,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1156
1202
|
requestId?: string;
|
|
1157
1203
|
rawBody?: string;
|
|
1158
1204
|
reason?: ProviderErrorReason;
|
|
1159
|
-
} = {};
|
|
1205
|
+
} = { cause: error };
|
|
1160
1206
|
if (retryAfterMs !== undefined) {
|
|
1161
1207
|
errorOptions.retryAfterMs = retryAfterMs;
|
|
1162
1208
|
}
|
|
@@ -1185,7 +1231,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1185
1231
|
formattedMessage,
|
|
1186
1232
|
this.name,
|
|
1187
1233
|
error.status,
|
|
1188
|
-
|
|
1234
|
+
errorOptions,
|
|
1189
1235
|
);
|
|
1190
1236
|
}
|
|
1191
1237
|
throw new ProviderError(
|
|
@@ -1300,7 +1346,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1300
1346
|
result.push({
|
|
1301
1347
|
role: "tool",
|
|
1302
1348
|
tool_call_id: tr.tool_use_id,
|
|
1303
|
-
content: serialized.payload,
|
|
1349
|
+
content: protectJsonSchemaToolResult(serialized.payload),
|
|
1304
1350
|
});
|
|
1305
1351
|
}
|
|
1306
1352
|
|
|
@@ -1387,16 +1433,16 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1387
1433
|
}
|
|
1388
1434
|
|
|
1389
1435
|
// An assistant message must carry `content` or `tool_calls`. A turn with
|
|
1390
|
-
// neither (e.g. reasoning-only,
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1436
|
+
// neither (e.g. reasoning-only, a Stop before any text, or a turn whose
|
|
1437
|
+
// text arrived as whitespace) would serialize to blank content with no
|
|
1438
|
+
// tool calls, which strict OpenAI-compatible backends reject. Reasoning
|
|
1439
|
+
// lives in a separate field and does not satisfy the constraint, and
|
|
1440
|
+
// whitespace-only content does not survive a validator that trims before
|
|
1441
|
+
// checking presence, so the placeholder covers both.
|
|
1396
1442
|
if (
|
|
1397
|
-
this.backfillEmptyAssistantContent &&
|
|
1398
1443
|
!result.tool_calls &&
|
|
1399
|
-
(result.content === null ||
|
|
1444
|
+
(result.content === null ||
|
|
1445
|
+
(typeof result.content === "string" && result.content.trim() === ""))
|
|
1400
1446
|
) {
|
|
1401
1447
|
result.content = EMPTY_ASSISTANT_TURN_PLACEHOLDER;
|
|
1402
1448
|
}
|
|
@@ -680,7 +680,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
680
680
|
});
|
|
681
681
|
}
|
|
682
682
|
const retryAfterMs = extractRetryAfterMs(error.headers);
|
|
683
|
+
// `cause` keeps the SDK error's errno-bearing chain reachable for
|
|
684
|
+
// network-shape classification (`isRetryableNetworkError` walks it).
|
|
683
685
|
const errorOptions: {
|
|
686
|
+
cause?: unknown;
|
|
684
687
|
retryAfterMs?: number;
|
|
685
688
|
abortReason?: unknown;
|
|
686
689
|
apiErrorCode?: string;
|
|
@@ -689,7 +692,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
689
692
|
requestId?: string;
|
|
690
693
|
rawBody?: string;
|
|
691
694
|
reason?: ProviderErrorReason;
|
|
692
|
-
} = {};
|
|
695
|
+
} = { cause: error };
|
|
693
696
|
if (retryAfterMs !== undefined) {
|
|
694
697
|
errorOptions.retryAfterMs = retryAfterMs;
|
|
695
698
|
}
|
|
@@ -718,7 +721,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
718
721
|
formattedMessage,
|
|
719
722
|
this.name,
|
|
720
723
|
error.status,
|
|
721
|
-
|
|
724
|
+
errorOptions,
|
|
722
725
|
);
|
|
723
726
|
}
|
|
724
727
|
throw new ProviderError(
|
|
@@ -143,7 +143,6 @@ export class OpenRouterProvider extends OpenAIChatCompletionsProvider {
|
|
|
143
143
|
streamTimeoutMs: options.streamTimeoutMs,
|
|
144
144
|
requestHeaders: OPENROUTER_APP_ATTRIBUTION_HEADERS,
|
|
145
145
|
assistantReasoningField: "reasoning",
|
|
146
|
-
backfillEmptyAssistantContent: true,
|
|
147
146
|
});
|
|
148
147
|
this.openRouterApiKey = apiKey;
|
|
149
148
|
this.resolvedBaseURL = baseURL;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { getConfig } from "../config/loader.js";
|
|
12
12
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
13
13
|
import { getDb } from "../persistence/db-connection.js";
|
|
14
|
+
import type { ProviderRouteAttribution } from "../util/errors.js";
|
|
14
15
|
import { getLogger } from "../util/logger.js";
|
|
15
16
|
import {
|
|
16
17
|
describeSubscriptionModelIncompatibility,
|
|
@@ -70,6 +71,16 @@ export class CallSiteConfiguredProvider implements Provider {
|
|
|
70
71
|
// (callers like the advisor consult gate on it). Fixed at construction.
|
|
71
72
|
public readonly supportsNativeWebSearch?: boolean;
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Route the inner adapter signs with, stamped at connection resolution.
|
|
76
|
+
* Forwarded live (not snapshotted) so consumers holding this wrapper —
|
|
77
|
+
* e.g. the save-time probe's billing gate — read the row dispatch
|
|
78
|
+
* actually selected.
|
|
79
|
+
*/
|
|
80
|
+
get routeAttribution(): ProviderRouteAttribution | undefined {
|
|
81
|
+
return this.inner.routeAttribution;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
constructor(
|
|
74
85
|
private readonly inner: Provider,
|
|
75
86
|
private readonly callSite: LLMCallSite,
|
package/src/providers/retry.ts
CHANGED
|
@@ -246,6 +246,12 @@ const RETRYABLE_PROVIDER_ERROR_REASONS = new Set<ProviderErrorReason>([
|
|
|
246
246
|
"rate_limited",
|
|
247
247
|
"overloaded",
|
|
248
248
|
"server_error",
|
|
249
|
+
// Transport failures that never reached the server (SDK connection
|
|
250
|
+
// errors, Gemini proxy interception). Deadline and cancellation shapes
|
|
251
|
+
// never carry this reason — they surface as reason-less aborts and
|
|
252
|
+
// short-circuit in isRetryableError before the reason check — so a
|
|
253
|
+
// 30-minute stream deadline failure is never retried through it.
|
|
254
|
+
"network_error",
|
|
249
255
|
]);
|
|
250
256
|
|
|
251
257
|
function isRetryableStreamError(error: unknown): boolean {
|
|
@@ -76,6 +76,14 @@ export interface SearchProviderCatalogEntry {
|
|
|
76
76
|
/** Privacy-policy URL surfaced in marketing data-sharing docs.
|
|
77
77
|
* BYOK providers only. */
|
|
78
78
|
readonly privacyPolicyUrl?: string;
|
|
79
|
+
/**
|
|
80
|
+
* When true, settings UIs show an optional API Base field. Empty /
|
|
81
|
+
* omitted base uses {@link defaultApiBase}.
|
|
82
|
+
*/
|
|
83
|
+
readonly supportsApiBase?: boolean;
|
|
84
|
+
/** Cloud default origin when `supportsApiBase` is true and the user
|
|
85
|
+
* leaves API Base empty. */
|
|
86
|
+
readonly defaultApiBase?: string;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
|
|
@@ -143,6 +151,18 @@ export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
|
|
|
143
151
|
fallbackOrder: 5,
|
|
144
152
|
privacyPolicyUrl: "https://keenable.ai/privacy",
|
|
145
153
|
},
|
|
154
|
+
{
|
|
155
|
+
id: "fastcrw",
|
|
156
|
+
displayName: "fastCRW",
|
|
157
|
+
kind: "byok",
|
|
158
|
+
apiKeyPrefix: "crw_live_...",
|
|
159
|
+
envVar: "FASTCRW_API_KEY",
|
|
160
|
+
secretKey: "fastcrw",
|
|
161
|
+
fallbackOrder: 6,
|
|
162
|
+
privacyPolicyUrl: "https://fastcrw.com/privacy",
|
|
163
|
+
supportsApiBase: true,
|
|
164
|
+
defaultApiBase: "https://api.fastcrw.com",
|
|
165
|
+
},
|
|
146
166
|
];
|
|
147
167
|
|
|
148
168
|
/** Provider ids accepted by the web-search config schema. */
|
|
@@ -46,7 +46,6 @@ export class VercelAIGatewayProvider extends OpenAIChatCompletionsProvider {
|
|
|
46
46
|
// Vercel's OpenAI-compat endpoint streams reasoning text in
|
|
47
47
|
// `delta.reasoning` (see its Advanced Configuration docs).
|
|
48
48
|
assistantReasoningField: "reasoning",
|
|
49
|
-
backfillEmptyAssistantContent: true,
|
|
50
49
|
});
|
|
51
50
|
this.gatewayApiKey = apiKey;
|
|
52
51
|
this.resolvedBaseURL = baseURL;
|
package/src/runtime/AGENTS.md
CHANGED
|
@@ -74,6 +74,7 @@ Host CU allows the assistant to proxy computer-use actions (screenshots, mouse/k
|
|
|
74
74
|
- **Resolution**: Clients execute the CU action on the host and respond via:
|
|
75
75
|
- `POST /v1/host-cu-result` — `{ requestId, axTree?, axDiff?, screenshot?, screenshotWidthPx?, screenshotHeightPx?, screenWidthPt?, screenHeightPt?, executionResult?, executionError?, secondaryWindows?, userGuidance? }`
|
|
76
76
|
- **Tracking**: Uses the same `pending-interactions` tracker as the other host proxy types, with `kind: "host_cu"`. Registration happens in `conversation-routes.ts` and the route handler is in `host-cu-routes.ts`.
|
|
77
|
+
- **Conversation-agnostic observation**: `observeHostScreen()` in `host-observe.ts` issues a `computer_use_observe` request with no conversation, for callers outside an agent turn (`HostCuProxy` only exists inside one). It takes the initiating actor's principal id and reaches only that actor's own `host_cu` clients: `pickSameUserAutoResolve` picks the default target and `enforceSameActorOrErrorResult` gates an explicitly named one, both before the request is registered or broadcast. Its pending interaction carries no `conversationId`, so `host-cu-routes.ts` hands the raw observation fields straight to the waiting caller instead of routing through a conversation's CU proxy. On the desktop side the ordinary `host_cu` executor services the request.
|
|
77
78
|
|
|
78
79
|
### Host browser (desktop proxy CDP execution)
|
|
79
80
|
|