@vellumai/assistant 0.11.6 → 0.11.7-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/ARCHITECTURE.md +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-resolver.test.ts +74 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +6 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +18 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +259 -11
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +213 -8
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/seed-providers.ts +261 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +85 -133
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +231 -3
- package/src/providers/inference/auth.ts +1 -0
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +75 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/retry.ts +956 -68
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +21 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/platform.ts +32 -13
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { ProviderError } from "../../../util/errors.js";
|
|
4
|
+
import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
|
|
5
|
+
|
|
6
|
+
type MockChunk = {
|
|
7
|
+
choices: Array<{
|
|
8
|
+
delta: { content?: string | null };
|
|
9
|
+
finish_reason?: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function makeStream(chunks: MockChunk[]): AsyncIterable<MockChunk> {
|
|
15
|
+
return {
|
|
16
|
+
async *[Symbol.asyncIterator]() {
|
|
17
|
+
for (const c of chunks) {
|
|
18
|
+
yield c;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OK_CHUNKS: MockChunk[] = [
|
|
25
|
+
{
|
|
26
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
27
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function stubProviderWithErrors(errors: unknown[]): {
|
|
32
|
+
provider: OpenAIChatCompletionsProvider;
|
|
33
|
+
requests: unknown[];
|
|
34
|
+
} {
|
|
35
|
+
const provider = new OpenAIChatCompletionsProvider("test-key", "test-model");
|
|
36
|
+
const requests: unknown[] = [];
|
|
37
|
+
const pending = [...errors];
|
|
38
|
+
(provider as unknown as { client: unknown }).client = {
|
|
39
|
+
chat: {
|
|
40
|
+
completions: {
|
|
41
|
+
create: async (params: unknown) => {
|
|
42
|
+
// Snapshot: the fallback mutates `params` between attempts.
|
|
43
|
+
requests.push(JSON.parse(JSON.stringify(params)));
|
|
44
|
+
const error = pending.shift();
|
|
45
|
+
if (error !== undefined) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return makeStream(OK_CHUNKS);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return { provider, requests };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Together's server-side renderer error for MiniMax M3, verbatim.
|
|
57
|
+
const CHAT_TEMPLATE_400 =
|
|
58
|
+
"Failed to apply chat template: invalid operation: object is not callable (in chat:22)";
|
|
59
|
+
|
|
60
|
+
function rejection(message: string, status = 400): Error {
|
|
61
|
+
return Object.assign(new Error(message), { status });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const TINY_PNG =
|
|
65
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
66
|
+
|
|
67
|
+
describe("chat-template rejection flatten fallback", () => {
|
|
68
|
+
test("retries once with flattened string content when the chat template rejects a text-only content-parts array", async () => {
|
|
69
|
+
// GIVEN an endpoint whose chat template 400s on structured message content
|
|
70
|
+
const { provider, requests } = stubProviderWithErrors([
|
|
71
|
+
rejection(CHAT_TEMPLATE_400),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// WHEN a multi-block user message of purely textual parts is sent
|
|
75
|
+
const response = await provider.sendMessage([
|
|
76
|
+
{
|
|
77
|
+
role: "user",
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text", text: "first paragraph" },
|
|
80
|
+
{ type: "text", text: "second paragraph" },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// THEN the first request carried a content-parts array
|
|
86
|
+
expect(requests).toHaveLength(2);
|
|
87
|
+
const first = requests[0] as {
|
|
88
|
+
messages: Array<{ content: unknown }>;
|
|
89
|
+
};
|
|
90
|
+
expect(Array.isArray(first.messages[0].content)).toBe(true);
|
|
91
|
+
|
|
92
|
+
// AND the retry flattened it to a plain string keeping all the text
|
|
93
|
+
const second = requests[1] as {
|
|
94
|
+
messages: Array<{ content: unknown }>;
|
|
95
|
+
};
|
|
96
|
+
const flattened = second.messages[0].content;
|
|
97
|
+
expect(flattened).toBe("first paragraph\n\nsecond paragraph");
|
|
98
|
+
|
|
99
|
+
// AND the retry succeeded
|
|
100
|
+
const text = response.content.find((b) => b.type === "text") as
|
|
101
|
+
| { type: "text"; text: string }
|
|
102
|
+
| undefined;
|
|
103
|
+
expect(text?.text).toBe("ok");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("does not retry when a content-parts array carries media (never silently drop an image)", async () => {
|
|
107
|
+
// GIVEN an endpoint whose chat template 400s on structured message content
|
|
108
|
+
const { provider, requests } = stubProviderWithErrors([
|
|
109
|
+
rejection(CHAT_TEMPLATE_400),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// WHEN a multi-block user message with an image part is sent
|
|
113
|
+
const promise = provider.sendMessage([
|
|
114
|
+
{
|
|
115
|
+
role: "user",
|
|
116
|
+
content: [
|
|
117
|
+
{ type: "text", text: "what is in this image?" },
|
|
118
|
+
{
|
|
119
|
+
type: "image",
|
|
120
|
+
source: {
|
|
121
|
+
type: "base64",
|
|
122
|
+
media_type: "image/png",
|
|
123
|
+
data: TINY_PNG,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// THEN the error propagates with no retry, so downstream classification
|
|
131
|
+
// can surface the capability mismatch instead of answering without the image
|
|
132
|
+
await expect(promise).rejects.toBeInstanceOf(ProviderError);
|
|
133
|
+
expect(requests).toHaveLength(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("does not retry when the messages are already plain strings", async () => {
|
|
137
|
+
// GIVEN a chat-template 400 on a request whose content is already a
|
|
138
|
+
// plain string (flattening would change nothing, so retrying is futile)
|
|
139
|
+
const { provider, requests } = stubProviderWithErrors([
|
|
140
|
+
rejection(CHAT_TEMPLATE_400),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
// WHEN a single-text-block user message is sent
|
|
144
|
+
const promise = provider.sendMessage([
|
|
145
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// THEN the error propagates as a ProviderError with no retry attempt
|
|
149
|
+
await expect(promise).rejects.toBeInstanceOf(ProviderError);
|
|
150
|
+
expect(requests).toHaveLength(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("does not retry on 5xx even when the message mentions the chat template", async () => {
|
|
154
|
+
// GIVEN a 500 whose body happens to mention the chat template (a server
|
|
155
|
+
// fault, not a request-shape rejection)
|
|
156
|
+
const { provider, requests } = stubProviderWithErrors([
|
|
157
|
+
rejection(CHAT_TEMPLATE_400, 500),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
// WHEN a multi-block user message is sent
|
|
161
|
+
const promise = provider.sendMessage([
|
|
162
|
+
{
|
|
163
|
+
role: "user",
|
|
164
|
+
content: [
|
|
165
|
+
{ type: "text", text: "a" },
|
|
166
|
+
{ type: "text", text: "b" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
// THEN the error propagates with no retry attempt
|
|
172
|
+
await expect(promise).rejects.toBeInstanceOf(ProviderError);
|
|
173
|
+
expect(requests).toHaveLength(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("surfaces the retry's error when the flattened request also fails", async () => {
|
|
177
|
+
// GIVEN an endpoint that rejects both the structured and the flattened shape
|
|
178
|
+
const { provider, requests } = stubProviderWithErrors([
|
|
179
|
+
rejection(CHAT_TEMPLATE_400),
|
|
180
|
+
rejection(CHAT_TEMPLATE_400),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
// WHEN a multi-block user message is sent
|
|
184
|
+
const promise = provider.sendMessage([
|
|
185
|
+
{
|
|
186
|
+
role: "user",
|
|
187
|
+
content: [
|
|
188
|
+
{ type: "text", text: "a" },
|
|
189
|
+
{ type: "text", text: "b" },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
// THEN the failure propagates after exactly one retry
|
|
195
|
+
await expect(promise).rejects.toBeInstanceOf(ProviderError);
|
|
196
|
+
expect(requests).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -4,6 +4,7 @@ import type { ProviderErrorReason } from "../../util/errors.js";
|
|
|
4
4
|
import {
|
|
5
5
|
DAILY_LIMIT_PATTERNS,
|
|
6
6
|
INSUFFICIENT_CREDITS_PATTERNS,
|
|
7
|
+
isChatTemplateFailureError,
|
|
7
8
|
VISION_NOT_SUPPORTED_PATTERNS,
|
|
8
9
|
} from "../../util/provider-error-patterns.js";
|
|
9
10
|
|
|
@@ -81,6 +82,18 @@ export function deriveReason(
|
|
|
81
82
|
return "vision_unsupported";
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// A 4xx from the endpoint's server-side chat-template renderer: the request
|
|
86
|
+
// shape (content-parts arrays, tool payloads) is what the template can't
|
|
87
|
+
// handle, not the credentials or the model.
|
|
88
|
+
if (
|
|
89
|
+
status !== undefined &&
|
|
90
|
+
status >= 400 &&
|
|
91
|
+
status < 500 &&
|
|
92
|
+
isChatTemplateFailureError(haystack)
|
|
93
|
+
) {
|
|
94
|
+
return "request_shape_unsupported";
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
// The managed proxy's daily-limit 402 shares the status with generic credit
|
|
85
98
|
// exhaustion; match its specific body code first so it isn't swallowed.
|
|
86
99
|
if (DAILY_LIMIT_PATTERNS.some((re) => re.test(haystack))) {
|
|
@@ -4,6 +4,7 @@ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
|
|
|
4
4
|
import { isAbortReason } from "../../util/abort-reasons.js";
|
|
5
5
|
import { ProviderError, type ProviderErrorReason } from "../../util/errors.js";
|
|
6
6
|
import { getLogger } from "../../util/logger.js";
|
|
7
|
+
import { isChatTemplateFailureError } from "../../util/provider-error-patterns.js";
|
|
7
8
|
import { extractRetryAfterMs } from "../../util/retry.js";
|
|
8
9
|
import { partialTagSuffix as sharedPartialTagSuffix } from "../../util/think-tag-stream.js";
|
|
9
10
|
import { escapeXmlAttr } from "../../util/xml.js";
|
|
@@ -529,6 +530,74 @@ function isUnknownAssistantReasoningFieldRejection(
|
|
|
529
530
|
);
|
|
530
531
|
}
|
|
531
532
|
|
|
533
|
+
function isTextualContentPart(part: { type: string }): boolean {
|
|
534
|
+
return part.type === "text" || part.type === "refusal";
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function messagesCarryFlattenableContentPartsArrays(params: unknown): boolean {
|
|
538
|
+
const messages = paramsMessages(params);
|
|
539
|
+
if (!messages) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
const arrays = messages.filter((msg) => Array.isArray(msg.content));
|
|
543
|
+
if (arrays.length === 0) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
return arrays.every((msg) =>
|
|
547
|
+
(msg.content as Array<{ type: string }>).every(isTextualContentPart),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* True when the endpoint's server-side chat-template renderer rejected the
|
|
553
|
+
* request and every content-parts array in the outbound messages is purely
|
|
554
|
+
* textual, so flattening to plain strings loses nothing. Some template
|
|
555
|
+
* engines behind OpenAI-compatible endpoints only render string message
|
|
556
|
+
* content (Together serving MiniMax M3 400s with `Failed to apply chat
|
|
557
|
+
* template: invalid operation: object is not callable`); one retry with
|
|
558
|
+
* flattened content lets the same endpoint succeed instead of surfacing the
|
|
559
|
+
* raw template error. Requests carrying media parts are never flattened:
|
|
560
|
+
* silently dropping an image or audio blob would let the model answer
|
|
561
|
+
* without it, so those rejections propagate to error classification.
|
|
562
|
+
*/
|
|
563
|
+
function isChatTemplateRejection(error: unknown, params: unknown): boolean {
|
|
564
|
+
if (!isClientErrorStatus(error)) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
if (!isChatTemplateFailureError(openaiCompatErrorHaystack(error))) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
return messagesCarryFlattenableContentPartsArrays(params);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Rewrite every purely-textual content-parts array in `params.messages` into
|
|
575
|
+
* a plain string. Returns whether any message changed.
|
|
576
|
+
*/
|
|
577
|
+
function flattenContentPartsToStrings(params: unknown): boolean {
|
|
578
|
+
const messages = paramsMessages(params);
|
|
579
|
+
if (!messages) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
let flattened = false;
|
|
583
|
+
for (const msg of messages) {
|
|
584
|
+
if (!Array.isArray(msg.content)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const parts = msg.content as Array<
|
|
588
|
+
{ type: "text"; text: string } | { type: "refusal"; refusal: string }
|
|
589
|
+
>;
|
|
590
|
+
if (!parts.every(isTextualContentPart)) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
msg.content = parts
|
|
594
|
+
.map((part) => (part.type === "text" ? part.text : part.refusal))
|
|
595
|
+
.join("\n\n");
|
|
596
|
+
flattened = true;
|
|
597
|
+
}
|
|
598
|
+
return flattened;
|
|
599
|
+
}
|
|
600
|
+
|
|
532
601
|
/**
|
|
533
602
|
* Translate the neutral (Anthropic-shaped) `tool_choice` carried on the call
|
|
534
603
|
* config into the OpenAI chat-completions wire format. Callers express
|
|
@@ -656,6 +725,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
656
725
|
const usageAttributionHeaders = configObj?.usageAttributionHeaders as
|
|
657
726
|
| Record<string, string>
|
|
658
727
|
| undefined;
|
|
728
|
+
const perRequestHeaders = configObj?.requestHeaders as
|
|
729
|
+
| Record<string, string>
|
|
730
|
+
| undefined;
|
|
659
731
|
|
|
660
732
|
// Per-tool keys whose object schemas were rewritten to JSON strings for the
|
|
661
733
|
// wire, to be decoded back on the response. Empty unless
|
|
@@ -844,6 +916,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
844
916
|
const requestHeaders = {
|
|
845
917
|
...this.requestHeaders,
|
|
846
918
|
...(usageAttributionHeaders ?? {}),
|
|
919
|
+
...(perRequestHeaders ?? {}),
|
|
847
920
|
};
|
|
848
921
|
const createStream = () =>
|
|
849
922
|
this.client.chat.completions.create(params, {
|
|
@@ -901,6 +974,17 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
901
974
|
);
|
|
902
975
|
stripAssistantReasoningFields(params);
|
|
903
976
|
stream = await createStream();
|
|
977
|
+
} else if (isChatTemplateRejection(error, params)) {
|
|
978
|
+
log.warn(
|
|
979
|
+
{
|
|
980
|
+
provider: this.name,
|
|
981
|
+
model: modelOverride ?? this.model,
|
|
982
|
+
error: error instanceof Error ? error.message : String(error),
|
|
983
|
+
},
|
|
984
|
+
"Upstream chat template rejected structured message content; retrying with flattened plain-text content",
|
|
985
|
+
);
|
|
986
|
+
flattenContentPartsToStrings(params);
|
|
987
|
+
stream = await createStream();
|
|
904
988
|
} else {
|
|
905
989
|
throw error;
|
|
906
990
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_ASSISTANT_TURN_PLACEHOLDER,
|
|
5
|
+
OpenAIChatCompletionsProvider,
|
|
6
|
+
} from "../openai/chat-completions-provider.js";
|
|
7
|
+
import {
|
|
8
|
+
buildOpenCodeRequestHeaders,
|
|
9
|
+
OPENCODE_GO_BASE_URL,
|
|
10
|
+
OPENCODE_REQUEST_HEADER,
|
|
11
|
+
OPENCODE_SESSION_HEADER,
|
|
12
|
+
OPENCODE_ZEN_BASE_URL,
|
|
13
|
+
OpenCodeProvider,
|
|
14
|
+
resolveOpenCodeBaseURL,
|
|
15
|
+
} from "./client.js";
|
|
16
|
+
|
|
17
|
+
describe("resolveOpenCodeBaseURL", () => {
|
|
18
|
+
test("defaults to OpenCode Zen", () => {
|
|
19
|
+
expect(resolveOpenCodeBaseURL()).toBe(OPENCODE_ZEN_BASE_URL);
|
|
20
|
+
expect(resolveOpenCodeBaseURL("")).toBe(OPENCODE_ZEN_BASE_URL);
|
|
21
|
+
expect(resolveOpenCodeBaseURL(" ")).toBe(OPENCODE_ZEN_BASE_URL);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("keeps a configured origin, including OpenCode Go", () => {
|
|
25
|
+
expect(resolveOpenCodeBaseURL(OPENCODE_GO_BASE_URL)).toBe(
|
|
26
|
+
OPENCODE_GO_BASE_URL,
|
|
27
|
+
);
|
|
28
|
+
expect(resolveOpenCodeBaseURL(" https://example.com/v1 ")).toBe(
|
|
29
|
+
"https://example.com/v1",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("buildOpenCodeRequestHeaders", () => {
|
|
35
|
+
test("omits headers when ids are missing", () => {
|
|
36
|
+
expect(buildOpenCodeRequestHeaders({})).toEqual({});
|
|
37
|
+
expect(buildOpenCodeRequestHeaders({ conversationId: " " })).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("sets session and request headers without session_id", () => {
|
|
41
|
+
const headers = buildOpenCodeRequestHeaders({
|
|
42
|
+
conversationId: "conv-xyz",
|
|
43
|
+
requestId: "req-123",
|
|
44
|
+
});
|
|
45
|
+
expect(headers[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
|
|
46
|
+
expect(headers[OPENCODE_REQUEST_HEADER]).toBe("req-123");
|
|
47
|
+
expect(headers).not.toHaveProperty("session_id");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("sets only the ids that are present", () => {
|
|
51
|
+
expect(
|
|
52
|
+
buildOpenCodeRequestHeaders({ conversationId: "conv-xyz" }),
|
|
53
|
+
).toEqual({ [OPENCODE_SESSION_HEADER]: "conv-xyz" });
|
|
54
|
+
expect(buildOpenCodeRequestHeaders({ requestId: "req-123" })).toEqual({
|
|
55
|
+
[OPENCODE_REQUEST_HEADER]: "req-123",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("OpenCodeProvider", () => {
|
|
61
|
+
test("uses OpenAI chat-completions with reasoning-compatible knobs", () => {
|
|
62
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
63
|
+
expect(provider).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
64
|
+
expect(provider.name).toBe("opencode");
|
|
65
|
+
expect(
|
|
66
|
+
(provider as unknown as { assistantReasoningField?: string })
|
|
67
|
+
.assistantReasoningField,
|
|
68
|
+
).toBe("reasoning_content");
|
|
69
|
+
expect(
|
|
70
|
+
(provider as unknown as { omitToolChoiceWhenReasoning: boolean })
|
|
71
|
+
.omitToolChoiceWhenReasoning,
|
|
72
|
+
).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("merges per-request OpenCode headers onto the transport", async () => {
|
|
76
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
77
|
+
const seen: Array<{ params: unknown; options: unknown }> = [];
|
|
78
|
+
(provider as unknown as { client: unknown }).client = {
|
|
79
|
+
chat: {
|
|
80
|
+
completions: {
|
|
81
|
+
create: async (params: unknown, options: unknown) => {
|
|
82
|
+
seen.push({ params, options });
|
|
83
|
+
return {
|
|
84
|
+
async *[Symbol.asyncIterator]() {
|
|
85
|
+
yield {
|
|
86
|
+
choices: [
|
|
87
|
+
{ delta: { content: "ok" }, finish_reason: "stop" },
|
|
88
|
+
],
|
|
89
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await provider.sendMessage(
|
|
99
|
+
[{ role: "user", content: [{ type: "text", text: "question" }] }],
|
|
100
|
+
{
|
|
101
|
+
config: {
|
|
102
|
+
requestHeaders: {
|
|
103
|
+
[OPENCODE_SESSION_HEADER]: "conv-xyz",
|
|
104
|
+
[OPENCODE_REQUEST_HEADER]: "req-123",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const options = seen[0]!.options as { headers?: Record<string, string> };
|
|
111
|
+
expect(options.headers?.[OPENCODE_SESSION_HEADER]).toBe("conv-xyz");
|
|
112
|
+
expect(options.headers?.[OPENCODE_REQUEST_HEADER]).toBe("req-123");
|
|
113
|
+
expect(options.headers).not.toHaveProperty("session_id");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("backfills placeholder content after an aborted empty assistant turn", async () => {
|
|
117
|
+
const provider = new OpenCodeProvider("sk-test", "mimo-v2.5-free");
|
|
118
|
+
const requests: unknown[] = [];
|
|
119
|
+
(provider as unknown as { client: unknown }).client = {
|
|
120
|
+
chat: {
|
|
121
|
+
completions: {
|
|
122
|
+
create: async (params: unknown) => {
|
|
123
|
+
requests.push(params);
|
|
124
|
+
return {
|
|
125
|
+
async *[Symbol.asyncIterator]() {
|
|
126
|
+
yield {
|
|
127
|
+
choices: [
|
|
128
|
+
{ delta: { content: "ok" }, finish_reason: "stop" },
|
|
129
|
+
],
|
|
130
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await provider.sendMessage([
|
|
140
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
141
|
+
{ role: "assistant", content: [] },
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const params = requests[0] as {
|
|
145
|
+
messages: Array<{ role: string; content: string | null }>;
|
|
146
|
+
};
|
|
147
|
+
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
148
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
2
|
+
|
|
3
|
+
export const OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
4
|
+
export const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
5
|
+
export const OPENCODE_SESSION_HEADER = "x-opencode-session";
|
|
6
|
+
export const OPENCODE_REQUEST_HEADER = "x-opencode-request";
|
|
7
|
+
|
|
8
|
+
export interface OpenCodeProviderOptions {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
streamTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the OpenCode chat-completions origin. A stored connection URL
|
|
15
|
+
* wins; otherwise the request goes to OpenCode Zen.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveOpenCodeBaseURL(configuredBaseURL?: string): string {
|
|
18
|
+
const trimmed = configuredBaseURL?.trim();
|
|
19
|
+
if (trimmed) {
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
return OPENCODE_ZEN_BASE_URL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* OpenCode-owned request headers for support lookup. Sends session and
|
|
27
|
+
* request ids only when they exist. Never sets `session_id` (zen/go
|
|
28
|
+
* returns 500 when that header is present).
|
|
29
|
+
*/
|
|
30
|
+
export function buildOpenCodeRequestHeaders(opts: {
|
|
31
|
+
conversationId?: string;
|
|
32
|
+
requestId?: string;
|
|
33
|
+
}): Record<string, string> {
|
|
34
|
+
const headers: Record<string, string> = {};
|
|
35
|
+
const session = opts.conversationId?.trim();
|
|
36
|
+
if (session) {
|
|
37
|
+
headers[OPENCODE_SESSION_HEADER] = session;
|
|
38
|
+
}
|
|
39
|
+
const requestId = opts.requestId?.trim();
|
|
40
|
+
if (requestId) {
|
|
41
|
+
headers[OPENCODE_REQUEST_HEADER] = requestId;
|
|
42
|
+
}
|
|
43
|
+
return headers;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class OpenCodeProvider extends OpenAIChatCompletionsProvider {
|
|
47
|
+
constructor(
|
|
48
|
+
apiKey: string,
|
|
49
|
+
model: string,
|
|
50
|
+
options: OpenCodeProviderOptions = {},
|
|
51
|
+
) {
|
|
52
|
+
super(apiKey, model, {
|
|
53
|
+
baseURL: resolveOpenCodeBaseURL(options.baseURL),
|
|
54
|
+
providerName: "opencode",
|
|
55
|
+
providerLabel: "OpenCode",
|
|
56
|
+
streamTimeoutMs: options.streamTimeoutMs,
|
|
57
|
+
assistantReasoningField: "reasoning_content",
|
|
58
|
+
omitToolChoiceWhenReasoning: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
2
|
+
import { VALID_CONNECTION_PROVIDERS } from "./inference/auth.js";
|
|
3
|
+
import { getConnection } from "./inference/connections.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Selection-time predicate for `ResolveCallSiteOpts.isResolvableProvider`:
|
|
7
|
+
* a provider value dispatches when it is a known vendor/identity or names a
|
|
8
|
+
* connection entry row. Permissive on DB unavailability so a transient blip
|
|
9
|
+
* never heals away a valid entry profile; dispatch soft-fails on its own.
|
|
10
|
+
*/
|
|
11
|
+
export function dispatchProviderResolvable(provider: string): boolean {
|
|
12
|
+
if (VALID_CONNECTION_PROVIDERS.includes(provider)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return getConnection(getDb(), provider) != null;
|
|
17
|
+
} catch {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|