@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -1,39 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One bounded
|
|
3
|
-
* the voice judges
|
|
4
|
-
*
|
|
5
|
-
* keeps its default
|
|
2
|
+
* One bounded question set to TypeSafe's System One model (Jev), shared by
|
|
3
|
+
* the voice judges and the automatic profile router. A caller runs its
|
|
4
|
+
* question only when its call site resolves to the TypeSafe provider; every
|
|
5
|
+
* failure comes back as a non-answer so the caller keeps its default
|
|
6
|
+
* behavior.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import type { LLMCallSite } from "
|
|
9
|
-
import {
|
|
10
|
-
import { resolveConfiguredProvider } from "../
|
|
11
|
-
import type { Provider, ProviderResponse } from "../
|
|
12
|
-
import {
|
|
9
|
+
import type { LLMCallSite } from "../../config/schemas/llm.js";
|
|
10
|
+
import { getLogger } from "../../util/logger.js";
|
|
11
|
+
import { resolveConfiguredProvider } from "../provider-send-message.js";
|
|
12
|
+
import type { Provider, ProviderResponse } from "../types.js";
|
|
13
|
+
import {
|
|
14
|
+
type JevJsonValue,
|
|
15
|
+
type JevQuestions,
|
|
16
|
+
noulFromAnswer,
|
|
17
|
+
} from "./client.js";
|
|
13
18
|
|
|
14
|
-
const log = getLogger("typesafe-
|
|
19
|
+
const log = getLogger("typesafe-ask");
|
|
15
20
|
|
|
16
21
|
const TYPESAFE_PROVIDER_NAME = "typesafe";
|
|
17
22
|
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export type TypesafeOutcome = "answered" | "unavailable" | "timeout" | "error";
|
|
24
|
+
|
|
25
|
+
export interface TypesafeAskResult {
|
|
26
|
+
outcome: TypesafeOutcome;
|
|
27
|
+
/** One entry per question id, present when the outcome is "answered". */
|
|
28
|
+
answers?: Record<string, unknown>;
|
|
29
|
+
latencyMs: number;
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
export interface TypesafeNoulResult {
|
|
25
|
-
outcome:
|
|
33
|
+
outcome: TypesafeOutcome;
|
|
26
34
|
/** Calibrated P(yes), present when the outcome is "answered". */
|
|
27
35
|
noul?: number;
|
|
28
36
|
latencyMs: number;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The call site's provider when it resolves to TypeSafe, else null. Judged
|
|
41
|
+
* on the dispatched adapter's name rather than the configured provider: a
|
|
42
|
+
* managed profile is configured as `vellum` and only substitutes the
|
|
43
|
+
* TypeSafe upstream at dispatch, so the configured name alone would report
|
|
44
|
+
* the managed Jev profile as unavailable.
|
|
45
|
+
*/
|
|
32
46
|
export async function resolveTypesafeProvider(
|
|
33
47
|
callSite: LLMCallSite,
|
|
34
48
|
): Promise<Provider | null> {
|
|
35
49
|
const resolved = await resolveConfiguredProvider(callSite);
|
|
36
|
-
return resolved?.
|
|
50
|
+
return resolved?.provider.name === TYPESAFE_PROVIDER_NAME
|
|
37
51
|
? resolved.provider
|
|
38
52
|
: null;
|
|
39
53
|
}
|
|
@@ -65,21 +79,25 @@ function answersFrom(
|
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
* Ask one noul question. Never rejects. The timeout is a hard race rather
|
|
70
|
-
* than trust in the provider honoring the abort, because callers hold user
|
|
71
|
-
* audio or background work on the answer.
|
|
72
|
-
*/
|
|
73
|
-
export async function askTypesafeNoul(args: {
|
|
82
|
+
export interface TypesafeAskArgs {
|
|
74
83
|
callSite: LLMCallSite;
|
|
75
84
|
conversationId: string;
|
|
76
85
|
state: JevJsonValue;
|
|
77
|
-
|
|
86
|
+
questions: JevQuestions;
|
|
78
87
|
timeoutMs: number;
|
|
79
88
|
signal?: AbortSignal;
|
|
80
89
|
/** Defaults to {@link resolveTypesafeProvider} for `callSite`. */
|
|
81
90
|
resolveProvider?: () => Promise<Provider | null>;
|
|
82
|
-
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Ask one question set. Never rejects. The timeout is a hard race rather
|
|
95
|
+
* than trust in the provider honoring the abort, because callers hold user
|
|
96
|
+
* audio or a turn's first token on the answer.
|
|
97
|
+
*/
|
|
98
|
+
export async function askTypesafe(
|
|
99
|
+
args: TypesafeAskArgs,
|
|
100
|
+
): Promise<TypesafeAskResult> {
|
|
83
101
|
const startedAt = Date.now();
|
|
84
102
|
const elapsed = () => Date.now() - startedAt;
|
|
85
103
|
const controller = new AbortController();
|
|
@@ -87,7 +105,7 @@ export async function askTypesafeNoul(args: {
|
|
|
87
105
|
? AbortSignal.any([args.signal, controller.signal])
|
|
88
106
|
: controller.signal;
|
|
89
107
|
|
|
90
|
-
const ask = async (): Promise<
|
|
108
|
+
const ask = async (): Promise<TypesafeAskResult> => {
|
|
91
109
|
try {
|
|
92
110
|
const provider = await (
|
|
93
111
|
args.resolveProvider ?? (() => resolveTypesafeProvider(args.callSite))
|
|
@@ -104,9 +122,7 @@ export async function askTypesafeNoul(args: {
|
|
|
104
122
|
type: "text",
|
|
105
123
|
text: JSON.stringify({
|
|
106
124
|
state: args.state,
|
|
107
|
-
questions:
|
|
108
|
-
answer: { type: "noul", instructions: args.instructions },
|
|
109
|
-
},
|
|
125
|
+
questions: args.questions,
|
|
110
126
|
}),
|
|
111
127
|
},
|
|
112
128
|
],
|
|
@@ -121,20 +137,20 @@ export async function askTypesafeNoul(args: {
|
|
|
121
137
|
signal,
|
|
122
138
|
},
|
|
123
139
|
);
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
140
|
+
const answers = answersFrom(response);
|
|
141
|
+
if (answers === null) {
|
|
126
142
|
log.warn(
|
|
127
143
|
{ callSite: args.callSite, conversationId: args.conversationId },
|
|
128
|
-
"TypeSafe
|
|
144
|
+
"TypeSafe returned no usable answers",
|
|
129
145
|
);
|
|
130
146
|
return { outcome: "error", latencyMs: elapsed() };
|
|
131
147
|
}
|
|
132
|
-
return { outcome: "answered",
|
|
148
|
+
return { outcome: "answered", answers, latencyMs: elapsed() };
|
|
133
149
|
} catch (err) {
|
|
134
150
|
if (!signal.aborted) {
|
|
135
151
|
log.warn(
|
|
136
152
|
{ err, callSite: args.callSite, conversationId: args.conversationId },
|
|
137
|
-
"TypeSafe
|
|
153
|
+
"TypeSafe question failed",
|
|
138
154
|
);
|
|
139
155
|
}
|
|
140
156
|
return { outcome: "error", latencyMs: elapsed() };
|
|
@@ -142,7 +158,7 @@ export async function askTypesafeNoul(args: {
|
|
|
142
158
|
};
|
|
143
159
|
|
|
144
160
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
145
|
-
const timedOut = new Promise<
|
|
161
|
+
const timedOut = new Promise<TypesafeAskResult>((resolve) => {
|
|
146
162
|
timer = setTimeout(() => {
|
|
147
163
|
controller.abort();
|
|
148
164
|
resolve({ outcome: "timeout", latencyMs: elapsed() });
|
|
@@ -154,3 +170,26 @@ export async function askTypesafeNoul(args: {
|
|
|
154
170
|
clearTimeout(timer);
|
|
155
171
|
}
|
|
156
172
|
}
|
|
173
|
+
|
|
174
|
+
/** Ask one yes/no question. Never rejects. */
|
|
175
|
+
export async function askTypesafeNoul(
|
|
176
|
+
args: Omit<TypesafeAskArgs, "questions"> & { instructions: string },
|
|
177
|
+
): Promise<TypesafeNoulResult> {
|
|
178
|
+
const { instructions, ...rest } = args;
|
|
179
|
+
const result = await askTypesafe({
|
|
180
|
+
...rest,
|
|
181
|
+
questions: { answer: { type: "noul", instructions } },
|
|
182
|
+
});
|
|
183
|
+
if (result.outcome !== "answered") {
|
|
184
|
+
return { outcome: result.outcome, latencyMs: result.latencyMs };
|
|
185
|
+
}
|
|
186
|
+
const noul = noulFromAnswer(result.answers?.answer);
|
|
187
|
+
if (noul === undefined) {
|
|
188
|
+
log.warn(
|
|
189
|
+
{ callSite: args.callSite, conversationId: args.conversationId },
|
|
190
|
+
"TypeSafe judge returned no usable answer",
|
|
191
|
+
);
|
|
192
|
+
return { outcome: "error", latencyMs: result.latencyMs };
|
|
193
|
+
}
|
|
194
|
+
return { outcome: "answered", noul, latencyMs: result.latencyMs };
|
|
195
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream finish reasons that mean the model tried to call a tool but the
|
|
3
|
+
* call did not parse.
|
|
4
|
+
*
|
|
5
|
+
* Gemini reports these as `MALFORMED_FUNCTION_CALL` (the call was not valid)
|
|
6
|
+
* and `UNEXPECTED_TOOL_CALL` (a call the request did not allow). The native
|
|
7
|
+
* Gemini API returns them as `candidate.finishReason`; OpenRouter normalizes
|
|
8
|
+
* them away in `finish_reason` and passes the raw value through in
|
|
9
|
+
* `native_finish_reason`. Such responses often carry the tail of the broken
|
|
10
|
+
* call as plain assistant text, so they must never be taken as a final answer.
|
|
11
|
+
* Providers throw a status-less `ProviderError` carrying
|
|
12
|
+
* {@link MALFORMED_TOOL_CALL_MESSAGE}, which the retry layer (`retry.ts`)
|
|
13
|
+
* resends once with a corrective note.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ProviderError } from "../util/errors.js";
|
|
17
|
+
|
|
18
|
+
/** Message fragment shared by the provider throw sites and the retry classifier. */
|
|
19
|
+
export const MALFORMED_TOOL_CALL_MESSAGE =
|
|
20
|
+
"Model emitted a malformed tool call";
|
|
21
|
+
|
|
22
|
+
const MALFORMED_TOOL_CALL_FINISH_REASONS = new Set([
|
|
23
|
+
"MALFORMED_FUNCTION_CALL",
|
|
24
|
+
"UNEXPECTED_TOOL_CALL",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether an upstream finish reason denotes a malformed tool call.
|
|
29
|
+
* Case- and whitespace-insensitive; a `null`/`undefined`/empty reason is false.
|
|
30
|
+
*/
|
|
31
|
+
export function isMalformedToolCallFinishReason(
|
|
32
|
+
finishReason: string | null | undefined,
|
|
33
|
+
): boolean {
|
|
34
|
+
if (!finishReason) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return MALFORMED_TOOL_CALL_FINISH_REASONS.has(
|
|
38
|
+
finishReason.trim().toUpperCase(),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Status-less provider error for a response that ended on a malformed tool
|
|
44
|
+
* call. The absent status marks it as a stream-content failure, which the
|
|
45
|
+
* retry layer classifies by message.
|
|
46
|
+
*/
|
|
47
|
+
export function malformedToolCallError(
|
|
48
|
+
provider: string,
|
|
49
|
+
finishReason: string,
|
|
50
|
+
): ProviderError {
|
|
51
|
+
return new ProviderError(
|
|
52
|
+
`${MALFORMED_TOOL_CALL_MESSAGE} (finish reason: ${finishReason})`,
|
|
53
|
+
provider,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PLATFORM_PROVIDER_META } from "./platform-proxy/constants.js";
|
|
2
2
|
|
|
3
3
|
export type LongContextMode =
|
|
4
|
-
"native-model"
|
|
4
|
+
| "native-model"
|
|
5
|
+
| "provider-request-option"
|
|
6
|
+
| "unsupported";
|
|
5
7
|
|
|
6
8
|
export interface CatalogModelPricingTier {
|
|
7
9
|
/**
|
|
@@ -243,6 +245,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
243
245
|
cacheReadPer1mTokens: 1,
|
|
244
246
|
},
|
|
245
247
|
},
|
|
248
|
+
{
|
|
249
|
+
id: "claude-opus-5-5",
|
|
250
|
+
displayName: "Claude Opus 5.5",
|
|
251
|
+
contextWindowTokens: 1000000,
|
|
252
|
+
maxOutputTokens: 128000,
|
|
253
|
+
longContextPricingThresholdTokens: 200000,
|
|
254
|
+
supportsThinking: true,
|
|
255
|
+
adaptiveThinkingOnly: true,
|
|
256
|
+
supportsCaching: true,
|
|
257
|
+
supportsVision: true,
|
|
258
|
+
supportsToolUse: true,
|
|
259
|
+
pricing: {
|
|
260
|
+
inputPer1mTokens: 4,
|
|
261
|
+
outputPer1mTokens: 20,
|
|
262
|
+
cacheWritePer1mTokens: 5,
|
|
263
|
+
cacheReadPer1mTokens: 0.2,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
246
266
|
{
|
|
247
267
|
id: "claude-opus-5",
|
|
248
268
|
displayName: "Claude Opus 5",
|
|
@@ -454,6 +474,66 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
454
474
|
],
|
|
455
475
|
},
|
|
456
476
|
},
|
|
477
|
+
{
|
|
478
|
+
id: "gpt-6-sol",
|
|
479
|
+
displayName: "GPT-6 Sol",
|
|
480
|
+
contextWindowTokens: 1050000,
|
|
481
|
+
maxOutputTokens: 128000,
|
|
482
|
+
longContextPricingThresholdTokens:
|
|
483
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
484
|
+
supportsThinking: true,
|
|
485
|
+
supportsCaching: true,
|
|
486
|
+
supportsVision: true,
|
|
487
|
+
supportsToolUse: true,
|
|
488
|
+
supportsPromptCacheBreakpoints: true,
|
|
489
|
+
maxEffort: "max",
|
|
490
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
491
|
+
pricing: {
|
|
492
|
+
inputPer1mTokens: 2,
|
|
493
|
+
outputPer1mTokens: 10,
|
|
494
|
+
cacheWritePer1mTokens: 2.5,
|
|
495
|
+
cacheReadPer1mTokens: 0.2,
|
|
496
|
+
tiers: [
|
|
497
|
+
{
|
|
498
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
499
|
+
inputPer1mTokens: 4,
|
|
500
|
+
outputPer1mTokens: 15,
|
|
501
|
+
cacheWritePer1mTokens: 5,
|
|
502
|
+
cacheReadPer1mTokens: 0.4,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: "gpt-6-luna",
|
|
509
|
+
displayName: "GPT-6 Luna",
|
|
510
|
+
contextWindowTokens: 1050000,
|
|
511
|
+
maxOutputTokens: 128000,
|
|
512
|
+
longContextPricingThresholdTokens:
|
|
513
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
514
|
+
supportsThinking: true,
|
|
515
|
+
supportsCaching: true,
|
|
516
|
+
supportsVision: true,
|
|
517
|
+
supportsToolUse: true,
|
|
518
|
+
supportsPromptCacheBreakpoints: true,
|
|
519
|
+
maxEffort: "max",
|
|
520
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
521
|
+
pricing: {
|
|
522
|
+
inputPer1mTokens: 0.1,
|
|
523
|
+
outputPer1mTokens: 0.5,
|
|
524
|
+
cacheWritePer1mTokens: 0.125,
|
|
525
|
+
cacheReadPer1mTokens: 0.01,
|
|
526
|
+
tiers: [
|
|
527
|
+
{
|
|
528
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
529
|
+
inputPer1mTokens: 0.2,
|
|
530
|
+
outputPer1mTokens: 0.75,
|
|
531
|
+
cacheWritePer1mTokens: 0.25,
|
|
532
|
+
cacheReadPer1mTokens: 0.02,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
457
537
|
// GPT-5.6 family (Sol / Terra / Luna). cacheRead is the 90% cached-read
|
|
458
538
|
// discount; cacheWrite is the 1.25x-input rate GPT-5.6+ bills for
|
|
459
539
|
// prompt tokens written to the cache (reported as `cache_write_tokens`
|
|
@@ -1207,6 +1287,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
1207
1287
|
cacheReadPer1mTokens: 1,
|
|
1208
1288
|
},
|
|
1209
1289
|
},
|
|
1290
|
+
{
|
|
1291
|
+
id: "anthropic/claude-opus-5.5",
|
|
1292
|
+
displayName: "Claude Opus 5.5",
|
|
1293
|
+
contextWindowTokens: 1000000,
|
|
1294
|
+
maxOutputTokens: 128000,
|
|
1295
|
+
longContextPricingThresholdTokens: 200000,
|
|
1296
|
+
supportsThinking: true,
|
|
1297
|
+
adaptiveThinkingOnly: true,
|
|
1298
|
+
supportsCaching: true,
|
|
1299
|
+
supportsVision: true,
|
|
1300
|
+
supportsToolUse: true,
|
|
1301
|
+
pricing: {
|
|
1302
|
+
inputPer1mTokens: 4,
|
|
1303
|
+
outputPer1mTokens: 20,
|
|
1304
|
+
cacheWritePer1mTokens: 5,
|
|
1305
|
+
cacheReadPer1mTokens: 0.2,
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1210
1308
|
{
|
|
1211
1309
|
id: "anthropic/claude-opus-5",
|
|
1212
1310
|
displayName: "Claude Opus 5",
|
|
@@ -1429,6 +1527,126 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
1429
1527
|
],
|
|
1430
1528
|
},
|
|
1431
1529
|
},
|
|
1530
|
+
{
|
|
1531
|
+
id: "openai/gpt-6-sol",
|
|
1532
|
+
displayName: "GPT-6 Sol",
|
|
1533
|
+
contextWindowTokens: 1050000,
|
|
1534
|
+
maxOutputTokens: 128000,
|
|
1535
|
+
longContextPricingThresholdTokens:
|
|
1536
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1537
|
+
supportsThinking: true,
|
|
1538
|
+
supportsCaching: true,
|
|
1539
|
+
supportsVision: true,
|
|
1540
|
+
supportsToolUse: true,
|
|
1541
|
+
supportsPromptCacheBreakpoints: true,
|
|
1542
|
+
maxEffort: "max",
|
|
1543
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
1544
|
+
pricing: {
|
|
1545
|
+
inputPer1mTokens: 2,
|
|
1546
|
+
outputPer1mTokens: 10,
|
|
1547
|
+
cacheWritePer1mTokens: 2.5,
|
|
1548
|
+
cacheReadPer1mTokens: 0.2,
|
|
1549
|
+
tiers: [
|
|
1550
|
+
{
|
|
1551
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1552
|
+
inputPer1mTokens: 4,
|
|
1553
|
+
outputPer1mTokens: 15,
|
|
1554
|
+
cacheWritePer1mTokens: 5,
|
|
1555
|
+
cacheReadPer1mTokens: 0.4,
|
|
1556
|
+
},
|
|
1557
|
+
],
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
id: "openai/gpt-6-sol-pro",
|
|
1562
|
+
displayName: "GPT-6 Sol Pro",
|
|
1563
|
+
contextWindowTokens: 1050000,
|
|
1564
|
+
maxOutputTokens: 128000,
|
|
1565
|
+
longContextPricingThresholdTokens:
|
|
1566
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1567
|
+
supportsThinking: true,
|
|
1568
|
+
supportsCaching: true,
|
|
1569
|
+
supportsVision: true,
|
|
1570
|
+
supportsToolUse: true,
|
|
1571
|
+
supportsPromptCacheBreakpoints: true,
|
|
1572
|
+
maxEffort: "max",
|
|
1573
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
1574
|
+
pricing: {
|
|
1575
|
+
inputPer1mTokens: 2,
|
|
1576
|
+
outputPer1mTokens: 10,
|
|
1577
|
+
cacheWritePer1mTokens: 2.5,
|
|
1578
|
+
cacheReadPer1mTokens: 0.2,
|
|
1579
|
+
tiers: [
|
|
1580
|
+
{
|
|
1581
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1582
|
+
inputPer1mTokens: 4,
|
|
1583
|
+
outputPer1mTokens: 15,
|
|
1584
|
+
cacheWritePer1mTokens: 5,
|
|
1585
|
+
cacheReadPer1mTokens: 0.4,
|
|
1586
|
+
},
|
|
1587
|
+
],
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
id: "openai/gpt-6-luna",
|
|
1592
|
+
displayName: "GPT-6 Luna",
|
|
1593
|
+
contextWindowTokens: 1050000,
|
|
1594
|
+
maxOutputTokens: 128000,
|
|
1595
|
+
longContextPricingThresholdTokens:
|
|
1596
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1597
|
+
supportsThinking: true,
|
|
1598
|
+
supportsCaching: true,
|
|
1599
|
+
supportsVision: true,
|
|
1600
|
+
supportsToolUse: true,
|
|
1601
|
+
supportsPromptCacheBreakpoints: true,
|
|
1602
|
+
maxEffort: "max",
|
|
1603
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
1604
|
+
pricing: {
|
|
1605
|
+
inputPer1mTokens: 0.1,
|
|
1606
|
+
outputPer1mTokens: 0.5,
|
|
1607
|
+
cacheWritePer1mTokens: 0.125,
|
|
1608
|
+
cacheReadPer1mTokens: 0.01,
|
|
1609
|
+
tiers: [
|
|
1610
|
+
{
|
|
1611
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1612
|
+
inputPer1mTokens: 0.2,
|
|
1613
|
+
outputPer1mTokens: 0.75,
|
|
1614
|
+
cacheWritePer1mTokens: 0.25,
|
|
1615
|
+
cacheReadPer1mTokens: 0.02,
|
|
1616
|
+
},
|
|
1617
|
+
],
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
id: "openai/gpt-6-luna-pro",
|
|
1622
|
+
displayName: "GPT-6 Luna Pro",
|
|
1623
|
+
contextWindowTokens: 1050000,
|
|
1624
|
+
maxOutputTokens: 128000,
|
|
1625
|
+
longContextPricingThresholdTokens:
|
|
1626
|
+
OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1627
|
+
supportsThinking: true,
|
|
1628
|
+
supportsCaching: true,
|
|
1629
|
+
supportsVision: true,
|
|
1630
|
+
supportsToolUse: true,
|
|
1631
|
+
supportsPromptCacheBreakpoints: true,
|
|
1632
|
+
maxEffort: "max",
|
|
1633
|
+
supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
|
|
1634
|
+
pricing: {
|
|
1635
|
+
inputPer1mTokens: 0.1,
|
|
1636
|
+
outputPer1mTokens: 0.5,
|
|
1637
|
+
cacheWritePer1mTokens: 0.125,
|
|
1638
|
+
cacheReadPer1mTokens: 0.01,
|
|
1639
|
+
tiers: [
|
|
1640
|
+
{
|
|
1641
|
+
inputTokenThreshold: OPENAI_LONG_CONTEXT_PRICING_THRESHOLD_TOKENS,
|
|
1642
|
+
inputPer1mTokens: 0.2,
|
|
1643
|
+
outputPer1mTokens: 0.75,
|
|
1644
|
+
cacheWritePer1mTokens: 0.25,
|
|
1645
|
+
cacheReadPer1mTokens: 0.02,
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1432
1650
|
// GPT-5.6 family (Sol / Terra / Luna). The `*-pro` slugs are the same
|
|
1433
1651
|
// underlying models served with `reasoning.mode: pro` at identical
|
|
1434
1652
|
// rates. cacheWrite is the 1.25x-input rate GPT-5.6+ bills for prompt
|
|
@@ -2140,6 +2358,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
|
|
|
2140
2358
|
cacheReadPer1mTokens: 1,
|
|
2141
2359
|
},
|
|
2142
2360
|
},
|
|
2361
|
+
{
|
|
2362
|
+
id: "anthropic/claude-opus-5.5",
|
|
2363
|
+
displayName: "Claude Opus 5.5",
|
|
2364
|
+
contextWindowTokens: 1000000,
|
|
2365
|
+
maxOutputTokens: 128000,
|
|
2366
|
+
longContextPricingThresholdTokens: 200000,
|
|
2367
|
+
supportsThinking: true,
|
|
2368
|
+
adaptiveThinkingOnly: true,
|
|
2369
|
+
supportsCaching: true,
|
|
2370
|
+
supportsVision: true,
|
|
2371
|
+
supportsToolUse: true,
|
|
2372
|
+
pricing: {
|
|
2373
|
+
inputPer1mTokens: 4,
|
|
2374
|
+
outputPer1mTokens: 20,
|
|
2375
|
+
cacheWritePer1mTokens: 5,
|
|
2376
|
+
cacheReadPer1mTokens: 0.2,
|
|
2377
|
+
},
|
|
2378
|
+
},
|
|
2143
2379
|
{
|
|
2144
2380
|
id: "anthropic/claude-opus-5",
|
|
2145
2381
|
displayName: "Claude Opus 5",
|
|
@@ -2602,6 +2838,16 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] =
|
|
|
2602
2838
|
supportsPlatformAuth: PLATFORM_PROVIDER_META[entry.id]?.managed === true,
|
|
2603
2839
|
}));
|
|
2604
2840
|
|
|
2841
|
+
/**
|
|
2842
|
+
* Providers that are routing identities rather than catalog owners. Mirrors
|
|
2843
|
+
* `ROUTING_IDENTITY_PROVIDERS` in `inference/auth.ts`, which cannot be
|
|
2844
|
+
* imported here without a cycle.
|
|
2845
|
+
*/
|
|
2846
|
+
const ROUTING_IDENTITY_PROVIDER_IDS: ReadonlySet<string> = new Set([
|
|
2847
|
+
"vellum",
|
|
2848
|
+
"chatgpt",
|
|
2849
|
+
]);
|
|
2850
|
+
|
|
2605
2851
|
/**
|
|
2606
2852
|
* Whether a catalog model produces free-form chat text. Unlisted providers
|
|
2607
2853
|
* and model ids default to true so custom endpoints and unknown snapshots
|
|
@@ -2614,9 +2860,16 @@ export function catalogModelSupportsText(
|
|
|
2614
2860
|
if (typeof provider !== "string" || typeof modelId !== "string") {
|
|
2615
2861
|
return true;
|
|
2616
2862
|
}
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2863
|
+
// A routing identity ("vellum", "chatgpt") owns no catalog models of the
|
|
2864
|
+
// upstream it dispatches to, so judge the model by its catalog owner: a
|
|
2865
|
+
// managed Jev profile is `vellum` + `jev-latest`, and `jev-latest` opts out
|
|
2866
|
+
// of text under `typesafe`.
|
|
2867
|
+
const catalogProvider = ROUTING_IDENTITY_PROVIDER_IDS.has(provider)
|
|
2868
|
+
? (getCatalogProviderForModel(modelId) ?? provider)
|
|
2869
|
+
: provider;
|
|
2870
|
+
const model = PROVIDER_CATALOG.find(
|
|
2871
|
+
(p) => p.id === catalogProvider,
|
|
2872
|
+
)?.models.find((m) => m.id === modelId);
|
|
2620
2873
|
return model?.supportsText !== false;
|
|
2621
2874
|
}
|
|
2622
2875
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { ProviderError } from "../../../util/errors.js";
|
|
4
|
+
import { MALFORMED_TOOL_CALL_MESSAGE } from "../../malformed-tool-call.js";
|
|
5
|
+
import { OpenAIChatCompletionsProvider } from "../chat-completions-provider.js";
|
|
6
|
+
|
|
7
|
+
type MockChunk = {
|
|
8
|
+
choices: Array<{
|
|
9
|
+
delta: { content?: string | null };
|
|
10
|
+
finish_reason?: string | null;
|
|
11
|
+
native_finish_reason?: string | null;
|
|
12
|
+
}>;
|
|
13
|
+
usage?: { prompt_tokens: number; completion_tokens: number };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MESSAGES = [
|
|
17
|
+
{
|
|
18
|
+
role: "user" as const,
|
|
19
|
+
content: [{ type: "text" as const, text: "upload the cover image" }],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function stubProvider(chunks: MockChunk[]): OpenAIChatCompletionsProvider {
|
|
24
|
+
const provider = new OpenAIChatCompletionsProvider(
|
|
25
|
+
"test-key",
|
|
26
|
+
"google/gemini-3.8-flash",
|
|
27
|
+
);
|
|
28
|
+
(provider as unknown as { client: unknown }).client = {
|
|
29
|
+
baseURL: "https://openrouter.example.com/api/v1",
|
|
30
|
+
chat: {
|
|
31
|
+
completions: {
|
|
32
|
+
create: async () => ({
|
|
33
|
+
async *[Symbol.asyncIterator]() {
|
|
34
|
+
for (const c of chunks) {
|
|
35
|
+
yield c;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return provider;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function finishChunks(
|
|
46
|
+
text: string,
|
|
47
|
+
finishReason: string,
|
|
48
|
+
nativeFinishReason: string,
|
|
49
|
+
): MockChunk[] {
|
|
50
|
+
return [
|
|
51
|
+
{ choices: [{ delta: { content: text } }] },
|
|
52
|
+
{
|
|
53
|
+
choices: [
|
|
54
|
+
{
|
|
55
|
+
delta: {},
|
|
56
|
+
finish_reason: finishReason,
|
|
57
|
+
native_finish_reason: nativeFinishReason,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
usage: { prompt_tokens: 1, completion_tokens: 2 },
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("OpenAIChatCompletionsProvider malformed tool-call finish", () => {
|
|
66
|
+
test("throws a status-less malformed tool-call error on native MALFORMED_FUNCTION_CALL", async () => {
|
|
67
|
+
const provider = stubProvider(
|
|
68
|
+
finishChunks(
|
|
69
|
+
",featuredImage:{_type:image},patch:true}]})",
|
|
70
|
+
"stop",
|
|
71
|
+
"MALFORMED_FUNCTION_CALL",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const error = await provider.sendMessage(MESSAGES).then(
|
|
76
|
+
() => undefined,
|
|
77
|
+
(e: unknown) => e,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(error).toBeInstanceOf(ProviderError);
|
|
81
|
+
const providerError = error as ProviderError;
|
|
82
|
+
expect(providerError.statusCode).toBeUndefined();
|
|
83
|
+
expect(providerError.message).toContain(MALFORMED_TOOL_CALL_MESSAGE);
|
|
84
|
+
expect(providerError.message).toContain("MALFORMED_FUNCTION_CALL");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("throws on native UNEXPECTED_TOOL_CALL", async () => {
|
|
88
|
+
const provider = stubProvider(
|
|
89
|
+
finishChunks("{}", "error", "UNEXPECTED_TOOL_CALL"),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await expect(provider.sendMessage(MESSAGES)).rejects.toThrow(
|
|
93
|
+
MALFORMED_TOOL_CALL_MESSAGE,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns the response normally on native STOP", async () => {
|
|
98
|
+
const provider = stubProvider(finishChunks("All done.", "stop", "STOP"));
|
|
99
|
+
|
|
100
|
+
const response = await provider.sendMessage(MESSAGES);
|
|
101
|
+
|
|
102
|
+
expect(response.stopReason).toBe("stop");
|
|
103
|
+
expect(response.content).toEqual([{ type: "text", text: "All done." }]);
|
|
104
|
+
});
|
|
105
|
+
});
|