@open-everywhere/adapters 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/aws/sso.ts +0 -2
- package/agent/instrumentation.ts +67 -4
- package/agent/model/approval-judge.ts +3 -3
- package/agent/model/dream-judge.ts +8 -6
- package/agent/model/gateway-probe.ts +97 -11
- package/agent/model/gateway-vision.ts +10 -6
- package/agent/model/gateway.ts +448 -34
- package/agent/model/meeting-gateway.ts +37 -7
- package/agent/model/model-config-check.ts +243 -0
- package/agent/model/target-url.ts +38 -0
- package/agent/model/transcription-routes.ts +18 -4
- package/agent/oe-oauth/errors.ts +1 -1
- package/agent/oe-oauth/token-vault.ts +99 -29
- package/agent/oe-oauth/user-auth-flow.ts +4 -3
- package/agent/operator-holders.ts +47 -0
- package/agent/postgres/neon.ts +1 -1
- package/dist/agent/aws/sso.d.ts +1 -2
- package/dist/agent/aws/sso.d.ts.map +1 -1
- package/dist/agent/aws/sso.js +0 -2
- package/dist/agent/aws/sso.js.map +1 -1
- package/dist/agent/instrumentation.d.ts +23 -1
- package/dist/agent/instrumentation.d.ts.map +1 -1
- package/dist/agent/instrumentation.js +59 -3
- package/dist/agent/instrumentation.js.map +1 -1
- package/dist/agent/model/approval-judge.d.ts.map +1 -1
- package/dist/agent/model/approval-judge.js +3 -3
- package/dist/agent/model/approval-judge.js.map +1 -1
- package/dist/agent/model/dream-judge.d.ts +2 -0
- package/dist/agent/model/dream-judge.d.ts.map +1 -1
- package/dist/agent/model/dream-judge.js +6 -4
- package/dist/agent/model/dream-judge.js.map +1 -1
- package/dist/agent/model/gateway-probe.d.ts +22 -4
- package/dist/agent/model/gateway-probe.d.ts.map +1 -1
- package/dist/agent/model/gateway-probe.js +78 -11
- package/dist/agent/model/gateway-probe.js.map +1 -1
- package/dist/agent/model/gateway-vision.d.ts +4 -2
- package/dist/agent/model/gateway-vision.d.ts.map +1 -1
- package/dist/agent/model/gateway-vision.js +6 -4
- package/dist/agent/model/gateway-vision.js.map +1 -1
- package/dist/agent/model/gateway.d.ts +85 -12
- package/dist/agent/model/gateway.d.ts.map +1 -1
- package/dist/agent/model/gateway.js +418 -30
- package/dist/agent/model/gateway.js.map +1 -1
- package/dist/agent/model/meeting-gateway.d.ts +8 -2
- package/dist/agent/model/meeting-gateway.d.ts.map +1 -1
- package/dist/agent/model/meeting-gateway.js +29 -7
- package/dist/agent/model/meeting-gateway.js.map +1 -1
- package/dist/agent/model/model-config-check.d.ts +57 -0
- package/dist/agent/model/model-config-check.d.ts.map +1 -0
- package/dist/agent/model/model-config-check.js +207 -0
- package/dist/agent/model/model-config-check.js.map +1 -0
- package/dist/agent/model/target-url.d.ts +10 -0
- package/dist/agent/model/target-url.d.ts.map +1 -0
- package/dist/agent/model/target-url.js +32 -0
- package/dist/agent/model/target-url.js.map +1 -0
- package/dist/agent/model/transcription-routes.d.ts +3 -0
- package/dist/agent/model/transcription-routes.d.ts.map +1 -1
- package/dist/agent/model/transcription-routes.js +16 -4
- package/dist/agent/model/transcription-routes.js.map +1 -1
- package/dist/agent/oe-oauth/errors.d.ts +0 -7
- package/dist/agent/oe-oauth/errors.d.ts.map +1 -1
- package/dist/agent/oe-oauth/errors.js +1 -1
- package/dist/agent/oe-oauth/errors.js.map +1 -1
- package/dist/agent/oe-oauth/token-vault.d.ts +4 -3
- package/dist/agent/oe-oauth/token-vault.d.ts.map +1 -1
- package/dist/agent/oe-oauth/token-vault.js +88 -24
- package/dist/agent/oe-oauth/token-vault.js.map +1 -1
- package/dist/agent/oe-oauth/user-auth-flow.d.ts.map +1 -1
- package/dist/agent/oe-oauth/user-auth-flow.js +3 -2
- package/dist/agent/oe-oauth/user-auth-flow.js.map +1 -1
- package/dist/agent/operator-holders.d.ts +13 -0
- package/dist/agent/operator-holders.d.ts.map +1 -0
- package/dist/agent/operator-holders.js +40 -0
- package/dist/agent/operator-holders.js.map +1 -0
- package/dist/agent/postgres/neon.d.ts +0 -15
- package/dist/agent/postgres/neon.d.ts.map +1 -1
- package/dist/agent/postgres/neon.js +1 -1
- package/dist/agent/postgres/neon.js.map +1 -1
- package/package.json +19 -2
package/agent/model/gateway.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
// The Vercel AI Gateway model backend — the reference `ModelBackend`.
|
|
2
|
-
//
|
|
1
|
+
// The Vercel AI Gateway model backend — the reference `ModelBackend`. It serves every `models.targets` entry of kind
|
|
2
|
+
// `vercel-ai-gateway` (OPE-2556): one provider per target, built at the target's first call from its `url`. The target's
|
|
3
|
+
// `providers.only` is a demand on every call through it; its `providers.order` and a route's `providerOrder` are
|
|
4
|
+
// preferences. A route or a stack role picks its target with `via` (absent: `gateway`).
|
|
5
|
+
//
|
|
6
|
+
// 🔴 THE ONE MODEL ADAPTER (OPE-2535). Every model call of this process reaches the gateway through THIS file:
|
|
7
|
+
// the turn model, the side calls (dream, facet and approval judges, meeting recap, image describer, gateway
|
|
8
|
+
// transcription), and a bare model id string (a declared subagent's `model`), which the AI SDK resolves through
|
|
9
|
+
// the default provider this module installs. That provider sends each bare id through the target of the role that
|
|
10
|
+
// names it, else the primary role's target (OPE-2566, core model/bare-model-via.ts). An app that registers no roles
|
|
11
|
+
// sends every bare id through `gateway`, and the process says so once when another target exists.
|
|
12
|
+
// Every model it hands out carries `models.dataPolicy` on every call (`policyModel`). A call that cannot carry the rows (a realtime client secret, a streaming-transcription token)
|
|
13
|
+
// refuses under a policy or a `providers.only` row. Two exceptions: under `eve dev` eve resolves a literal id through its own local gateway, and a
|
|
14
|
+
// default provider another module set first is kept behind a guard that refuses under a policy, and for a bare id whose target is not `gateway`. `npm run lint:model-adapter` refuses a module elsewhere that builds a gateway model itself.
|
|
3
15
|
|
|
4
16
|
import { gateway, createGateway } from "ai";
|
|
5
17
|
import {
|
|
@@ -7,19 +19,33 @@ import {
|
|
|
7
19
|
type BindableContext,
|
|
8
20
|
type ModelBackend,
|
|
9
21
|
type RoutedCallOptions,
|
|
22
|
+
type ModelDataPolicy,
|
|
23
|
+
type ModelTarget,
|
|
24
|
+
DEFAULT_MODEL_VIA,
|
|
25
|
+
bareModelRoutesRegistered,
|
|
26
|
+
bareModelVia,
|
|
27
|
+
dataPolicyActive,
|
|
28
|
+
dataPolicyRows,
|
|
29
|
+
mergeDataPolicy,
|
|
30
|
+
getSettings,
|
|
10
31
|
hostPlaneFetch,
|
|
11
32
|
hostPlanePolicy,
|
|
12
33
|
liveIdentity,
|
|
34
|
+
modelTargetFor,
|
|
13
35
|
resolvePrimaryModel,
|
|
36
|
+
useSelfHostEgress,
|
|
14
37
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
38
|
+
import { targetBaseUrl } from "./target-url.js";
|
|
39
|
+
|
|
40
|
+
export { DEPRECATED_GATEWAY_URL_ENV, targetBaseUrl } from "./target-url.js";
|
|
15
41
|
|
|
16
42
|
type GatewayModel = ReturnType<typeof gateway>;
|
|
43
|
+
type GatewayProvider = typeof gateway;
|
|
17
44
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const GATEWAY_BASE_URL = process.env.OE_AI_GATEWAY_URL?.trim();
|
|
45
|
+
/** The deployment's targets, read at CALL time: settings register after this module loads. */
|
|
46
|
+
function currentTargets(): ReturnType<typeof getSettings>["models"]["targets"] {
|
|
47
|
+
return getSettings().models.targets;
|
|
48
|
+
}
|
|
23
49
|
|
|
24
50
|
/** The gateway's own host when {@link GATEWAY_BASE_URL} is unset. It must match the `ai` package's default base URL. */
|
|
25
51
|
export const DEFAULT_GATEWAY_HOST = "ai-gateway.vercel.sh";
|
|
@@ -34,8 +60,23 @@ export function gatewayHost(baseUrl: string | undefined): string {
|
|
|
34
60
|
}
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
/** The
|
|
38
|
-
|
|
63
|
+
/** The base URL each target resolved at its first use. READ ONCE per process: a target that changed mid-process
|
|
64
|
+
* would send two calls in one turn to two places. A change needs a restart. */
|
|
65
|
+
const TARGET_URLS = new Map<string, string | undefined>();
|
|
66
|
+
function resolvedTargetUrl(name: string): string | undefined {
|
|
67
|
+
if (!TARGET_URLS.has(name)) TARGET_URLS.set(name, targetBaseUrl(name, modelTargetFor(name, currentTargets())));
|
|
68
|
+
return TARGET_URLS.get(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The one host the `gateway` target's calls reach (its `url`, else the deprecated env var, else the default). */
|
|
72
|
+
export function gatewayTargetHost(): string {
|
|
73
|
+
return gatewayHost(resolvedTargetUrl(DEFAULT_MODEL_VIA));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The hosts of every configured target — what every host-plane push must keep declaring. */
|
|
77
|
+
function targetHosts(): string[] {
|
|
78
|
+
return Object.keys(currentTargets()).map((name) => gatewayHost(resolvedTargetUrl(name)));
|
|
79
|
+
}
|
|
39
80
|
|
|
40
81
|
/**
|
|
41
82
|
* THE HOST-PLANE UNION (OPE-2470). The proxy REPLACES its host plane on every push; it never merges
|
|
@@ -44,9 +85,17 @@ export const GATEWAY_TARGET_HOST = gatewayHost(GATEWAY_BASE_URL);
|
|
|
44
85
|
* non-gateway transcription routes never push lists that drop each other. A caller adds its hosts with
|
|
45
86
|
* {@link sharedHostPlaneFetch} at boot; a grown set builds a new fetch, which pushes the union on its next call.
|
|
46
87
|
*/
|
|
47
|
-
const HOST_PLANE_HOSTS: string[] = [
|
|
48
|
-
let hostPlaneCurrent
|
|
49
|
-
let hostPlaneBuiltFor =
|
|
88
|
+
const HOST_PLANE_HOSTS: string[] = [];
|
|
89
|
+
let hostPlaneCurrent: typeof fetch | undefined;
|
|
90
|
+
let hostPlaneBuiltFor = -1;
|
|
91
|
+
let targetHostsSeeded = false;
|
|
92
|
+
/** Add every target's host to the host plane, once. At CALL time: the targets are settings, which register after this
|
|
93
|
+
* module loads. */
|
|
94
|
+
function seedTargetHosts(): void {
|
|
95
|
+
if (targetHostsSeeded) return;
|
|
96
|
+
targetHostsSeeded = true;
|
|
97
|
+
for (const h of targetHosts()) if (!HOST_PLANE_HOSTS.includes(h)) HOST_PLANE_HOSTS.unshift(h);
|
|
98
|
+
}
|
|
50
99
|
/**
|
|
51
100
|
* THE MODEL CALL'S TRANSPORT ON THE SELF-HOST PROXY TIER (OPE-2406). With an egress proxy wired, the model call goes out
|
|
52
101
|
* on the app's HOST PLANE: the proxy decides it against the host-plane list (the gateway host, plus the hosts of any
|
|
@@ -54,11 +103,11 @@ let hostPlaneBuiltFor = HOST_PLANE_HOSTS.length;
|
|
|
54
103
|
* its model while the session stays default-deny. Off that tier it is `undefined` and the provider keeps its default
|
|
55
104
|
* fetch, byte-identical to before. On it, the fetch always uses the newest union.
|
|
56
105
|
*/
|
|
57
|
-
const HOST_PLANE_FETCH: typeof fetch | undefined =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (hostPlaneBuiltFor !== HOST_PLANE_HOSTS.length) {
|
|
106
|
+
const HOST_PLANE_FETCH: typeof fetch | undefined = !useSelfHostEgress()
|
|
107
|
+
? undefined
|
|
108
|
+
: (input, init) => {
|
|
109
|
+
seedTargetHosts();
|
|
110
|
+
if (hostPlaneCurrent === undefined || hostPlaneBuiltFor !== HOST_PLANE_HOSTS.length) {
|
|
62
111
|
hostPlaneCurrent = hostPlaneFetch(HOST_PLANE_HOSTS);
|
|
63
112
|
hostPlaneBuiltFor = HOST_PLANE_HOSTS.length;
|
|
64
113
|
}
|
|
@@ -70,22 +119,386 @@ export function sharedHostPlaneFetch(hosts: readonly string[]): typeof fetch | u
|
|
|
70
119
|
// Validate BEFORE the list grows: a wildcard host throws here, at registration, and never reaches the shared list,
|
|
71
120
|
// where it would fail every model turn at the next rebuild. A blank name is skipped.
|
|
72
121
|
const named = hosts.map((h) => h.trim()).filter((h) => h !== "");
|
|
73
|
-
if (HOST_PLANE_FETCH !== undefined) hostPlanePolicy(
|
|
122
|
+
if (HOST_PLANE_FETCH !== undefined && named.length > 0) hostPlanePolicy(named);
|
|
74
123
|
for (const h of named) if (!HOST_PLANE_HOSTS.includes(h)) HOST_PLANE_HOSTS.push(h);
|
|
75
124
|
return HOST_PLANE_FETCH;
|
|
76
125
|
}
|
|
77
126
|
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
/** One gateway provider per target, built at the target's first call. A target with no url off the proxy tier is the
|
|
128
|
+
* plain `gateway` export, byte-identical to before. */
|
|
129
|
+
const TARGET_PROVIDERS = new Map<string, GatewayProvider>();
|
|
130
|
+
function targetProvider(via: string | undefined): GatewayProvider {
|
|
131
|
+
const name = via ?? DEFAULT_MODEL_VIA;
|
|
132
|
+
let p = TARGET_PROVIDERS.get(name);
|
|
133
|
+
if (p === undefined) {
|
|
134
|
+
const baseURL = resolvedTargetUrl(name);
|
|
135
|
+
p =
|
|
136
|
+
baseURL !== undefined || HOST_PLANE_FETCH !== undefined
|
|
137
|
+
? createGateway({ ...(baseURL !== undefined ? { baseURL } : {}), ...(HOST_PLANE_FETCH !== undefined ? { fetch: HOST_PLANE_FETCH } : {}) })
|
|
138
|
+
: gateway;
|
|
139
|
+
TARGET_PROVIDERS.set(name, p);
|
|
140
|
+
}
|
|
141
|
+
return p;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── the data policy (OPE-2535) ─────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The Vercel AI Gateway options for one `models.dataPolicy`. Pure. An absent row adds no key, so the shipped `{}`
|
|
148
|
+
* changes no request byte.
|
|
149
|
+
*/
|
|
150
|
+
export function vercelDataPolicyOptions(p: ModelDataPolicy): Record<string, unknown> {
|
|
151
|
+
return {
|
|
152
|
+
...(p.zeroRetention === true ? { zeroDataRetention: true } : {}),
|
|
153
|
+
...(p.noTraining === true ? { disallowPromptTraining: true } : {}),
|
|
154
|
+
...(p.region !== undefined ? { inferenceRegion: { scope: "zone", geoRegion: p.region } } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The call-options shape every model kind shares: language, transcription and embedding calls all carry it. */
|
|
159
|
+
type ProviderOptionsCarrier = { readonly providerOptions?: Record<string, unknown> | undefined };
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Put the policy on one call's options. Pure. The policy's keys WIN over the call's own gateway keys, so a call
|
|
163
|
+
* cannot weaken a row (`zeroDataRetention: false` on a call is overwritten). Every other key passes through. A
|
|
164
|
+
* policy that demands nothing returns the options unchanged.
|
|
165
|
+
*/
|
|
166
|
+
export function withDataPolicy<T extends ProviderOptionsCarrier>(options: T, p: ModelDataPolicy): T {
|
|
167
|
+
return withTargetRows(options, undefined, p);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether a target or a policy demands anything of a call: a `models.dataPolicy` row, a `providers.only` row, or a
|
|
171
|
+
* row of the target's own `dataPolicy`. */
|
|
172
|
+
function demandsAnything(t: ModelTarget | undefined, p: ModelDataPolicy): boolean {
|
|
173
|
+
return dataPolicyActive(p) || (t?.providers?.only?.length ?? 0) > 0 || (t?.dataPolicy !== undefined && dataPolicyActive(t.dataPolicy));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The demands on a call, in the words of the settings file, for an error: `models.dataPolicy (region eu)`,
|
|
178
|
+
* `models.targets.gateway.providers.only (openai, azure)`, `models.targets.mistral.dataPolicy (zeroRetention)`, or
|
|
179
|
+
* several joined with " + ".
|
|
180
|
+
*/
|
|
181
|
+
export function demandLabel(p: ModelDataPolicy, t?: ModelTarget, via: string = DEFAULT_MODEL_VIA): string {
|
|
182
|
+
const parts: string[] = [];
|
|
183
|
+
if (dataPolicyActive(p)) parts.push(`models.dataPolicy (${dataPolicyRows(p).join(", ")})`);
|
|
184
|
+
const only = t?.providers?.only;
|
|
185
|
+
if (only !== undefined && only.length > 0) parts.push(`models.targets.${via}.providers.only (${only.join(", ")})`);
|
|
186
|
+
if (t?.dataPolicy !== undefined && dataPolicyActive(t.dataPolicy)) parts.push(`models.targets.${via}.dataPolicy (${dataPolicyRows(t.dataPolicy).join(", ")})`);
|
|
187
|
+
return parts.join(" + ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Put a target's rows and the data policy on one call's options. Pure but for a refusal. The order of the merge:
|
|
192
|
+
* 1. the target's `options` are a FLOOR: a call's own key wins;
|
|
193
|
+
* 2. the target's `providers.order` applies when the call names no `order` (a route's `providerOrder` wins);
|
|
194
|
+
* 3. the target's `providers.only` is a DEMAND: a call's own `only` narrows it to the intersection, and an empty
|
|
195
|
+
* intersection REFUSES (an empty `only` would read as no filter at all);
|
|
196
|
+
* 4. the policy's keys win over everything. The policy is `models.dataPolicy` plus the target's own `dataPolicy` rows
|
|
197
|
+
* (OPE-2531): a target adds rows and never removes one.
|
|
198
|
+
* A target with no rows and a policy that demands nothing return the options unchanged, so the shipped value changes
|
|
199
|
+
* no request byte.
|
|
200
|
+
*/
|
|
201
|
+
export function withTargetRows<T extends ProviderOptionsCarrier>(options: T, t: ModelTarget | undefined, global: ModelDataPolicy, via: string = DEFAULT_MODEL_VIA): T {
|
|
202
|
+
const floor = t?.options;
|
|
203
|
+
const only = t?.providers?.only;
|
|
204
|
+
const order = t?.providers?.order;
|
|
205
|
+
let p: ModelDataPolicy;
|
|
206
|
+
try {
|
|
207
|
+
p = mergeDataPolicy(global, t?.dataPolicy);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new Error(`models.targets.${via}.dataPolicy: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
210
|
+
}
|
|
211
|
+
if (floor === undefined && only === undefined && order === undefined && !dataPolicyActive(p)) return options;
|
|
212
|
+
const gw = (options.providerOptions?.["gateway"] ?? {}) as Record<string, unknown>;
|
|
213
|
+
const merged: Record<string, unknown> = { ...floor, ...gw };
|
|
214
|
+
if (order !== undefined && merged["order"] === undefined) merged["order"] = [...order];
|
|
215
|
+
if (only !== undefined) {
|
|
216
|
+
const own = Array.isArray(gw["only"]) ? (gw["only"] as unknown[]) : undefined;
|
|
217
|
+
const allowed = own === undefined ? [...only] : own.filter((s): s is string => typeof s === "string" && only.includes(s));
|
|
218
|
+
if (allowed.length === 0) {
|
|
219
|
+
throw new Error(`${demandLabel(global, t, via)}: the call's own provider list (${(own ?? []).join(", ")}) shares no provider with the target's, so it refuses`);
|
|
220
|
+
}
|
|
221
|
+
merged["only"] = allowed;
|
|
222
|
+
}
|
|
223
|
+
Object.assign(merged, vercelDataPolicyOptions(p));
|
|
224
|
+
return { ...options, providerOptions: { ...options.providerOptions, gateway: merged } };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The gateway's refusal when no provider meets a filter (a 400 `invalid_request_error`). Live text, 2026-10-14: "No provider for <id> supports the
|
|
228
|
+
* requested inference region." and "No available providers match the 'only' filter". The SDK keeps only the
|
|
229
|
+
* message (the error type is the generic `invalid_request_error`), so the match is on the text. */
|
|
230
|
+
const NO_PROVIDER = /\bno (?:available )?providers?\b/i;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Name the policy rows on a refusal the policy caused. A model with no provider that meets the rows fails with an
|
|
234
|
+
* error that says which rows, and which model: "models.dataPolicy (region eu): no provider serves …". Any other
|
|
235
|
+
* error, or any error with no policy set, passes through unchanged.
|
|
236
|
+
*/
|
|
237
|
+
export function dataPolicyCallError(modelId: string, p: ModelDataPolicy, err: unknown, t?: ModelTarget, via?: string): unknown {
|
|
238
|
+
if (!demandsAnything(t, p)) return err;
|
|
239
|
+
// Only a non-retryable 4xx is the policy's refusal. A 5xx with the same words is an outage: it passes unchanged,
|
|
240
|
+
// with its status and its retry flag, and it does not blame the rows.
|
|
241
|
+
const e = err as { statusCode?: unknown; isRetryable?: unknown };
|
|
242
|
+
if (typeof e?.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500 || e.isRetryable === true) return err;
|
|
243
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
244
|
+
if (!NO_PROVIDER.test(msg)) return err;
|
|
245
|
+
return new Error(`${demandLabel(p, t, via)}: no provider serves "${modelId}" under these rows. The call fails closed and does not fall back. Gateway said: ${msg}`, { cause: err });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The deployment's data policy, read at CALL time: settings register after this module loads. */
|
|
249
|
+
function currentDataPolicy(): ModelDataPolicy {
|
|
250
|
+
return getSettings().models.dataPolicy;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The model methods that send a request: every `do*` member (`doGenerate`, `doStream`, `doEmbed`, `doEvaluate`,
|
|
254
|
+
* `doRerank`, video's `doStart`, batch's `doStartBatch`, …). Each takes the call options first. */
|
|
255
|
+
const CALL_METHOD = /^do[A-Z]/;
|
|
256
|
+
|
|
257
|
+
/** Call methods that send no `providerOptions` at all, so the rows cannot ride them: the realtime client secret.
|
|
258
|
+
* Under a policy they refuse. */
|
|
259
|
+
const ROWLESS_METHODS = new Set<string>(["doCreateClientSecret"]);
|
|
260
|
+
|
|
261
|
+
/** Put the rows on a call's options, and for a batch on each request's own options too: the SDK sends a batch
|
|
262
|
+
* request's `providerOptions` per request, so the top-level merge alone would leave a request's own `false`. */
|
|
263
|
+
function withRowsFor(prop: string, options: ProviderOptionsCarrier, t: ModelTarget | undefined, p: ModelDataPolicy, via: string): ProviderOptionsCarrier {
|
|
264
|
+
const merged = withTargetRows(options, t, p, via);
|
|
265
|
+
const requests = (merged as { requests?: unknown }).requests;
|
|
266
|
+
if (prop !== "doStartBatch" || merged === options || !Array.isArray(requests)) return merged;
|
|
267
|
+
return {
|
|
268
|
+
...merged,
|
|
269
|
+
requests: requests.map((r: unknown) => {
|
|
270
|
+
const req = r as { options?: ProviderOptionsCarrier } | null;
|
|
271
|
+
return req !== null && typeof req === "object" ? { ...req, options: withTargetRows(req.options ?? {}, t, p, via) } : r;
|
|
272
|
+
}),
|
|
273
|
+
} as ProviderOptionsCarrier;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The target a `via` names, read at CALL time. An unknown name throws: a call never guesses a target. */
|
|
277
|
+
function currentTarget(via: string): ModelTarget {
|
|
278
|
+
return modelTargetFor(via, currentTargets());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Copy a provider member's own properties onto its wrapper. A function among them (`experimental_transcription.getToken`)
|
|
283
|
+
* mints a session token from the RAW model, and the client then opens the session itself, so the rows cannot ride it:
|
|
284
|
+
* under a policy it refuses. Used by both the policy provider and the foreign-provider guard.
|
|
285
|
+
*/
|
|
286
|
+
function withGuardedMemberProps<W extends (...a: unknown[]) => unknown>(wrapped: W, member: object, name: string, via: string = DEFAULT_MODEL_VIA): W {
|
|
287
|
+
for (const [k, v] of Object.entries(member)) {
|
|
288
|
+
(wrapped as unknown as Record<string, unknown>)[k] =
|
|
289
|
+
typeof v === "function"
|
|
290
|
+
? (...args: unknown[]): unknown => {
|
|
291
|
+
const p = currentDataPolicy();
|
|
292
|
+
const t = currentTarget(via);
|
|
293
|
+
if (demandsAnything(t, p)) throw rowlessRefusal(p, `${name}.${k}`, t, via);
|
|
294
|
+
return (v as (...a: unknown[]) => unknown).apply(member, args);
|
|
295
|
+
}
|
|
296
|
+
: v;
|
|
297
|
+
}
|
|
298
|
+
return wrapped;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Refuse a call that cannot carry the rows. */
|
|
302
|
+
function rowlessRefusal(p: ModelDataPolicy, what: string, t?: ModelTarget, via?: string): Error {
|
|
303
|
+
return new Error(`${demandLabel(p, t, via)}: ${what} cannot carry the rows, so it refuses`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A model that carries the data policy on EVERY call it sends. A Proxy, so the model keeps its class, its
|
|
308
|
+
* `provider`, its `modelId` and every other member: eve and the AI SDK see the same object shape. Only the calls
|
|
309
|
+
* change: the policy merges into the options, and a refusal it caused names the rows.
|
|
310
|
+
*/
|
|
311
|
+
export function policyModel<M extends object>(model: M, via: string = DEFAULT_MODEL_VIA): M {
|
|
312
|
+
return new Proxy(model, {
|
|
313
|
+
get(target, prop, receiver) {
|
|
314
|
+
const value = Reflect.get(target, prop, receiver) as unknown;
|
|
315
|
+
if (typeof prop !== "string" || !CALL_METHOD.test(prop) || typeof value !== "function") return value;
|
|
316
|
+
return async (options: unknown, ...rest: unknown[]) => {
|
|
317
|
+
const p = currentDataPolicy();
|
|
318
|
+
const t = currentTarget(via);
|
|
319
|
+
// A first argument that is not an options object has no `providerOptions` to carry the rows: refuse it under a
|
|
320
|
+
// demand rather than send it bare.
|
|
321
|
+
const isOptions = options !== null && typeof options === "object" && !Array.isArray(options);
|
|
322
|
+
if ((!isOptions || ROWLESS_METHODS.has(prop)) && demandsAnything(t, p)) throw rowlessRefusal(p, prop, t, via);
|
|
323
|
+
const sent = isOptions ? withRowsFor(prop, options as ProviderOptionsCarrier, t, p, via) : options;
|
|
324
|
+
try {
|
|
325
|
+
return await (value as (...a: unknown[]) => Promise<unknown>).call(receiver, sent, ...rest);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
throw dataPolicyCallError(String((target as { modelId?: unknown }).modelId ?? "?"), p, err, t, via);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** A gateway LANGUAGE model on the target `via` names (absent: `gateway`), with its url and the host-plane fetch,
|
|
335
|
+
* carrying the target's rows and the data policy. The one way a module outside this file gets a gateway model. */
|
|
336
|
+
export function gatewayLanguageModel(id: string, via?: string): GatewayModel {
|
|
337
|
+
return policyModel(targetProvider(via)(id), via ?? DEFAULT_MODEL_VIA);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Marks the policy provider, across bundle copies of this module, so a second copy does not mistake it for a
|
|
341
|
+
* foreign provider. Answered by the proxy; the gateway provider itself is never written to. */
|
|
342
|
+
const OWN_PROVIDER = Symbol.for("open-everywhere.gateway.policyProvider");
|
|
343
|
+
|
|
344
|
+
/** The provider members that build nothing that sends a prompt: account reads and the tool catalogue. */
|
|
345
|
+
const NON_MODEL_MEMBERS = new Set<PropertyKey>(["getAvailableModels", "getCredits", "getSpendReport", "getGenerationInfo", "tools"]);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* A provider whose every model-building member (`languageModel`, its `chat` alias, `evaluationModel`, `reranking`,
|
|
349
|
+
* `experimental_batch`, …) hands out policy models. A denylist, not an allow-list: a member the SDK adds later is
|
|
350
|
+
* wrapped by default. A member's own properties (`experimental_transcription.getToken`) stay on it.
|
|
351
|
+
*/
|
|
352
|
+
export function policyProvider<P extends object>(base: P): P {
|
|
353
|
+
return new Proxy(base, {
|
|
354
|
+
apply(target, thisArg, args: unknown[]) {
|
|
355
|
+
const m = Reflect.apply(target as (...a: unknown[]) => unknown, thisArg, args);
|
|
356
|
+
return m !== null && typeof m === "object" ? policyModel(m) : m;
|
|
357
|
+
},
|
|
358
|
+
get(target, prop, receiver) {
|
|
359
|
+
if (prop === OWN_PROVIDER) return true;
|
|
360
|
+
const value = Reflect.get(target, prop, receiver) as unknown;
|
|
361
|
+
if (typeof value !== "function" || NON_MODEL_MEMBERS.has(prop)) return value;
|
|
362
|
+
const wrapped = (...args: unknown[]): unknown => {
|
|
363
|
+
const m = (value as (...a: unknown[]) => unknown).apply(target, args);
|
|
364
|
+
return m !== null && typeof m === "object" ? policyModel(m) : m;
|
|
365
|
+
};
|
|
366
|
+
return withGuardedMemberProps(wrapped, value, String(prop));
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The target a member call's bare id goes through: its first argument, else the deployment default. */
|
|
372
|
+
function bareViaOf(args: readonly unknown[]): string {
|
|
373
|
+
noteUnregisteredBareIds();
|
|
374
|
+
return bareModelVia(typeof args[0] === "string" ? args[0] : "").via;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let bareGapNoted = false;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* An app that does not register its roles (`agent/model/models.ts` from before 0.8.0) sends every bare id through
|
|
381
|
+
* `gateway`. When `models.targets` names another target, that may leave the target a role names, so the process says so
|
|
382
|
+
* once, at the first bare-id call, and the call goes on through `gateway` (OPE-2566). An all-gateway app logs nothing.
|
|
383
|
+
*/
|
|
384
|
+
function noteUnregisteredBareIds(): void {
|
|
385
|
+
if (bareGapNoted || bareModelRoutesRegistered()) return;
|
|
386
|
+
if (!Object.keys(currentTargets()).some((k) => k !== DEFAULT_MODEL_VIA)) return;
|
|
387
|
+
bareGapNoted = true;
|
|
388
|
+
console.error(
|
|
389
|
+
"[open-everywhere:model] a bare model id (a declared subagent's model, eve evaluate()) goes via gateway, because agent/model/models.ts " +
|
|
390
|
+
"does not call registerBareModelRoutes. Add the line from the 0.8.0 template so it takes its role's target. npm run check:model-config names this too.",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Test seam: let the next bare-id call note the gap again. */
|
|
395
|
+
export function _resetBareGapNoticeForTest(): void {
|
|
396
|
+
bareGapNoted = false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* THE BARE-ID PROVIDER (OPE-2566). A provider that sends each bare id through the target its role names
|
|
401
|
+
* (core model/bare-model-via.ts): the role that names the id, else the primary role's target, else `gateway`. Every
|
|
402
|
+
* model it builds is a policy model on that target, so the target's url, provider pin and data-policy rows ride the
|
|
403
|
+
* call. It reads the table at CALL time: the app registers its roles after this module loads. A member's own
|
|
404
|
+
* properties (`experimental_transcription.getToken`) and the account reads use the deployment default's target.
|
|
405
|
+
* Exported for tests.
|
|
406
|
+
*/
|
|
407
|
+
export function bareIdProvider(): GatewayProvider {
|
|
408
|
+
const build = (prop: PropertyKey | undefined, args: unknown[]): unknown => {
|
|
409
|
+
const via = bareViaOf(args);
|
|
410
|
+
const p = targetProvider(via);
|
|
411
|
+
const m = prop === undefined ? Reflect.apply(p as unknown as (...a: unknown[]) => unknown, undefined, args) : (Reflect.get(p, prop) as (...a: unknown[]) => unknown).apply(p, args);
|
|
412
|
+
return m !== null && typeof m === "object" ? policyModel(m, via) : m;
|
|
413
|
+
};
|
|
414
|
+
return new Proxy(function bareIdGateway() {} as unknown as GatewayProvider, {
|
|
415
|
+
apply: (_t, _thisArg, args: unknown[]) => build(undefined, args),
|
|
416
|
+
get: (_t, prop) => {
|
|
417
|
+
if (prop === OWN_PROVIDER) return true;
|
|
418
|
+
const via = bareModelVia("").via;
|
|
419
|
+
const value = Reflect.get(targetProvider(via), prop) as unknown;
|
|
420
|
+
if (typeof value !== "function" || NON_MODEL_MEMBERS.has(prop)) return value;
|
|
421
|
+
return withGuardedMemberProps((...args: unknown[]): unknown => build(prop, args), value, String(prop), via);
|
|
422
|
+
},
|
|
423
|
+
has: (_t, prop) => prop === OWN_PROVIDER || Reflect.has(targetProvider(bareModelVia("").via), prop),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* THE DEFAULT PROVIDER. The AI SDK resolves a bare model id string through `globalThis.AI_SDK_DEFAULT_PROVIDER`,
|
|
429
|
+
* else its own plain `gateway`. eve passes a declared subagent's `model: "<id>"` to the SDK as that string, and
|
|
430
|
+
* eve's own `evaluate()` (auto-approval, auto routing, agentRouter) does the same. Without this those calls skip the
|
|
431
|
+
* data policy, the retarget and the host plane. This installs the bare-id provider there, so each call also goes
|
|
432
|
+
* through its role's target (OPE-2566).
|
|
433
|
+
*
|
|
434
|
+
* 🔴 A PROVIDER ANOTHER MODULE ALREADY SET is kept, because that module chose it. But it is not this adapter's, so
|
|
435
|
+
* it cannot carry the rows: under a data policy every call through it REFUSES (fail closed), and the boot says so.
|
|
436
|
+
*/
|
|
437
|
+
const sdkGlobal = globalThis as { AI_SDK_DEFAULT_PROVIDER?: unknown };
|
|
438
|
+
const installed = sdkGlobal.AI_SDK_DEFAULT_PROVIDER;
|
|
439
|
+
if (installed === undefined || installed === null) {
|
|
440
|
+
sdkGlobal.AI_SDK_DEFAULT_PROVIDER = bareIdProvider();
|
|
441
|
+
} else if ((typeof installed === "object" || typeof installed === "function") && installed !== null && (installed as Record<symbol, unknown>)[OWN_PROVIDER] !== true) {
|
|
442
|
+
console.error(
|
|
443
|
+
"[open-everywhere:model] AI_SDK_DEFAULT_PROVIDER was already set by another module. A bare model id resolves through it, " +
|
|
444
|
+
"and it cannot carry models.dataPolicy, so under a data policy every such call refuses.",
|
|
445
|
+
);
|
|
446
|
+
sdkGlobal.AI_SDK_DEFAULT_PROVIDER = foreignProviderGuard(installed);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A foreign default provider, kept, behind a guard: with no policy it is untouched; under a policy every model call
|
|
451
|
+
* through it refuses, because nothing proves that provider honours the rows. The guard reads the target the bare id
|
|
452
|
+
* goes through (OPE-2566): that target's rows count, and a target other than `gateway` refuses too, because the
|
|
453
|
+
* foreign provider would send the call somewhere else. Exported for tests.
|
|
454
|
+
*/
|
|
455
|
+
export function foreignProviderGuard<P extends object>(base: P): P {
|
|
456
|
+
const guardModel = (m: object, via: string): object =>
|
|
457
|
+
new Proxy(m, {
|
|
458
|
+
get(target, prop, receiver) {
|
|
459
|
+
const value = Reflect.get(target, prop, receiver) as unknown;
|
|
460
|
+
if (typeof prop !== "string" || !CALL_METHOD.test(prop) || typeof value !== "function") return value;
|
|
461
|
+
return async (...args: unknown[]) => {
|
|
462
|
+
if (via !== DEFAULT_MODEL_VIA) {
|
|
463
|
+
throw new Error(`this call goes through the target "${via}", but it resolved through a default provider that is not the gateway adapter's, so it cannot reach that target, and it refuses`);
|
|
464
|
+
}
|
|
465
|
+
const p = currentDataPolicy();
|
|
466
|
+
const t = currentTarget(via);
|
|
467
|
+
if (demandsAnything(t, p)) {
|
|
468
|
+
throw new Error(`${demandLabel(p, t)}: this call resolved through a default provider that is not the gateway adapter's, so it cannot carry the rows, and it refuses`);
|
|
469
|
+
}
|
|
470
|
+
return await (value as (...a: unknown[]) => Promise<unknown>).apply(receiver, args);
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
return new Proxy(base, {
|
|
475
|
+
apply(target, thisArg, args: unknown[]) {
|
|
476
|
+
const m = Reflect.apply(target as (...a: unknown[]) => unknown, thisArg, args);
|
|
477
|
+
return m !== null && typeof m === "object" ? guardModel(m, bareViaOf(args)) : m;
|
|
478
|
+
},
|
|
479
|
+
get(target, prop, receiver) {
|
|
480
|
+
// A second bundle copy of this module must not guard (and report) the same provider twice.
|
|
481
|
+
if (prop === OWN_PROVIDER) return true;
|
|
482
|
+
const value = Reflect.get(target, prop, receiver) as unknown;
|
|
483
|
+
if (typeof value !== "function" || NON_MODEL_MEMBERS.has(prop)) return value;
|
|
484
|
+
return withGuardedMemberProps(
|
|
485
|
+
(...args: unknown[]): unknown => {
|
|
486
|
+
const m = (value as (...a: unknown[]) => unknown).apply(target, args);
|
|
487
|
+
return m !== null && typeof m === "object" ? guardModel(m, bareViaOf(args)) : m;
|
|
488
|
+
},
|
|
489
|
+
value,
|
|
490
|
+
String(prop),
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
84
495
|
|
|
85
496
|
/** Static defaults for one routed slot — mirrors what a plain string config said. */
|
|
86
497
|
export interface GatewayRoutedDefaults {
|
|
87
|
-
/** Default gateway model id (what unrouted channels use), e.g. "
|
|
498
|
+
/** Default gateway model id (what unrouted channels use), e.g. "openai/gpt-6-luna". */
|
|
88
499
|
readonly id: string;
|
|
500
|
+
/** The `models.targets` entry the role's calls go through (OPE-2556). A route's own `via` wins. Absent: `gateway`. */
|
|
501
|
+
readonly via?: string;
|
|
89
502
|
/**
|
|
90
503
|
* Gateway providerOptions applied when the CALL carries none — a floor, since turn calls carry their own.
|
|
91
504
|
* Any gateway option is forwarded verbatim; this mapping has no allow-list of keys.
|
|
@@ -98,7 +511,8 @@ const PIN_KEYS = ["only", "order"] as const;
|
|
|
98
511
|
|
|
99
512
|
/**
|
|
100
513
|
* Map a route's provider hints onto one call's gateway options. Pure. A MODEL CHANGE strips the pin keys — a
|
|
101
|
-
* pin set for one model is broken for another — and a route's `
|
|
514
|
+
* pin set for one model is broken for another — and a route's `providerOrder` becomes `order`, never `only`. The
|
|
515
|
+
* target's own rows (`providers.only`, `providers.order`, `options`) join later, on every call (`withTargetRows`).
|
|
102
516
|
*/
|
|
103
517
|
export function applyGatewayOptions(
|
|
104
518
|
route: ModelRoute,
|
|
@@ -112,8 +526,8 @@ export function applyGatewayOptions(
|
|
|
112
526
|
gatewayOpts = { ...base };
|
|
113
527
|
for (const k of PIN_KEYS) delete gatewayOpts[k];
|
|
114
528
|
}
|
|
115
|
-
if (route.
|
|
116
|
-
gatewayOpts = { ...(gatewayOpts ?? {}), order: [...route.
|
|
529
|
+
if (route.providerOrder !== undefined) {
|
|
530
|
+
gatewayOpts = { ...(gatewayOpts ?? {}), order: [...route.providerOrder] };
|
|
117
531
|
delete gatewayOpts["only"];
|
|
118
532
|
}
|
|
119
533
|
if (gatewayOpts === undefined) return options;
|
|
@@ -122,14 +536,14 @@ export function applyGatewayOptions(
|
|
|
122
536
|
|
|
123
537
|
/** A gateway TRANSCRIPTION model, on the same provider (retarget and host-plane fetch included) the turn
|
|
124
538
|
* model uses. The meeting bot's `gateway` route (meeting-gateway.ts) reads it. */
|
|
125
|
-
export function gatewayTranscriptionModel(id: string): ReturnType<
|
|
126
|
-
return
|
|
539
|
+
export function gatewayTranscriptionModel(id: string): ReturnType<GatewayProvider["transcriptionModel"]> {
|
|
540
|
+
return policyModel(targetProvider(DEFAULT_MODEL_VIA).transcriptionModel(id));
|
|
127
541
|
}
|
|
128
542
|
|
|
129
|
-
/** The gateway ModelBackend (see core ModelBackend). */
|
|
130
|
-
export function gatewayModelBackend(defaults?: Record<string, unknown
|
|
543
|
+
/** The gateway ModelBackend (see core ModelBackend). `via` is the role's target; a route's own `via` wins. */
|
|
544
|
+
export function gatewayModelBackend(defaults?: Record<string, unknown>, via?: string): ModelBackend<GatewayModel> {
|
|
131
545
|
return {
|
|
132
|
-
model: (id) =>
|
|
546
|
+
model: (id, routeVia) => gatewayLanguageModel(id, routeVia ?? via),
|
|
133
547
|
applyOptions: (route, options, ids) => applyGatewayOptions(route, options, ids, defaults),
|
|
134
548
|
};
|
|
135
549
|
}
|
|
@@ -139,7 +553,7 @@ export function gatewayModelBackend(defaults?: Record<string, unknown>): ModelBa
|
|
|
139
553
|
* scope. It ALWAYS returns a model, never null, and `event` carries the turn id this ctx does not.
|
|
140
554
|
*/
|
|
141
555
|
export function gatewayModelResolver(primary: GatewayRoutedDefaults): (event: unknown, ctx: unknown) => Promise<GatewayModel> {
|
|
142
|
-
const deps = { ...liveIdentity, backend: gatewayModelBackend(primary.gateway) };
|
|
556
|
+
const deps = { ...liveIdentity, backend: gatewayModelBackend(primary.gateway, primary.via) };
|
|
143
557
|
return (event, ctx) => resolvePrimaryModel(ctx as BindableContext, { primary: { id: primary.id } }, deps, event);
|
|
144
558
|
}
|
|
145
559
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// The meeting bot's two model calls (OPE-2459): the `gateway` transcription route, and the recap completion.
|
|
2
|
-
//
|
|
1
|
+
// The meeting bot's two model calls (OPE-2459): the `gateway` transcription route, and the recap completion. Plus the
|
|
2
|
+
// voice-clip transcriber (OPE-2524), which rides the same transcription role. Core owns every seam
|
|
3
|
+
// (meetings/transcription.ts, channels/voice-clips.ts); this file only fills them, so no meeting or chat code calls a
|
|
4
|
+
// model SDK.
|
|
3
5
|
//
|
|
4
6
|
// TRANSCRIPTION (OPE-2470): one audio chunk per call, along the STACK'S TRANSCRIPTION ROLE — a primary route and a
|
|
5
7
|
// fallback route (transcription-routes.ts), named per stack in the app's `agent/model/stacks.ts`. A provider outage
|
|
@@ -10,12 +12,23 @@
|
|
|
10
12
|
// LEGACY: `registerGatewayMeetingModels(defaults)` with no role keeps the one-model route on the policy's
|
|
11
13
|
// `transcription.model`, for an app that has no transcription role yet.
|
|
12
14
|
//
|
|
15
|
+
// VOICE CLIPS (OPE-2524): a chat message's voice clip rides the SAME role, through {@link voiceClipTranscriber}. A
|
|
16
|
+
// clip is one call, like one meeting chunk, so it gets the same primary, the same fallback and the same timeouts.
|
|
17
|
+
//
|
|
13
18
|
// RECAP: one tool-less completion on the scope's `utility` route — the same route and call shape the dream
|
|
14
19
|
// judge uses. The prompt carries the fenced, untrusted transcript (core meetings/recap.ts).
|
|
15
20
|
|
|
16
|
-
import { experimental_transcribe as transcribe, generateText
|
|
17
|
-
import {
|
|
18
|
-
|
|
21
|
+
import { experimental_transcribe as transcribe, generateText } from "ai";
|
|
22
|
+
import {
|
|
23
|
+
registerMeetingCompletion,
|
|
24
|
+
registerTranscriptionRoute,
|
|
25
|
+
registerVoiceTranscriber,
|
|
26
|
+
type MeetingCompletion,
|
|
27
|
+
type TranscriptionInput,
|
|
28
|
+
type TranscriptionRoute,
|
|
29
|
+
type VoiceTranscriber,
|
|
30
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
31
|
+
import { gatewayLanguageModel, gatewayTranscriptionModel, type GatewayRoutedDefaults } from "./gateway.js";
|
|
19
32
|
import { resolvedDreamCallSettings } from "./dream-judge.js";
|
|
20
33
|
import { roleDisclosure, transcribeWithFallback, type RoleTranscript, type TranscriptionDeps, type TranscriptionRoleSpec } from "./transcription-routes.js";
|
|
21
34
|
|
|
@@ -65,11 +78,24 @@ export function stackTranscriptionRoute(role: TranscriptionRoleSpec, deps: Trans
|
|
|
65
78
|
};
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The voice-clip transcriber (core channels/voice-clips.ts) on the stack's transcription role. The spend row names the
|
|
83
|
+
* model of the route that ANSWERED, so a fall-through bills under the fallback's model and not the primary's.
|
|
84
|
+
*/
|
|
85
|
+
export function voiceClipTranscriber(role: TranscriptionRoleSpec, deps: TranscriptionDeps = {}): VoiceTranscriber {
|
|
86
|
+
const run = transcribeWithFallback(role, deps);
|
|
87
|
+
return async (clip) => {
|
|
88
|
+
// The message's deadline rides as the CALLER's abort, so a call past it stops and does not start the fallback.
|
|
89
|
+
const r = await run({ audio: clip.audio, mediaType: clip.mediaType, ...(clip.abortSignal ? { abortSignal: clip.abortSignal } : {}) });
|
|
90
|
+
return { text: r.text, model: r.route.id, ...(r.costUsd !== undefined ? { costUsd: r.costUsd } : {}) };
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
function gatewayMeetingCompletion(defaults: GatewayRoutedDefaults): MeetingCompletion {
|
|
69
95
|
return async (scope, prompt) => {
|
|
70
96
|
const s = resolvedDreamCallSettings(scope, defaults);
|
|
71
97
|
const { text } = await generateText({
|
|
72
|
-
model:
|
|
98
|
+
model: gatewayLanguageModel(s.id, s.via),
|
|
73
99
|
prompt,
|
|
74
100
|
...(s.reasoning !== undefined ? { reasoning: s.reasoning } : {}),
|
|
75
101
|
...(s.providerOptions !== undefined ? { providerOptions: s.providerOptions as Parameters<typeof generateText>[0]["providerOptions"] } : {}),
|
|
@@ -80,9 +106,13 @@ function gatewayMeetingCompletion(defaults: GatewayRoutedDefaults): MeetingCompl
|
|
|
80
106
|
|
|
81
107
|
/**
|
|
82
108
|
* Boot registration from agent.ts, beside the dream judge: the recap rides the `utility` defaults, and the
|
|
83
|
-
* transcription route rides the stack's transcription role when the app passes one.
|
|
109
|
+
* transcription route rides the stack's transcription role when the app passes one. The role ALSO registers the
|
|
110
|
+
* voice-clip transcriber (OPE-2524), so an app that already passes its role hears voice clips with no app change.
|
|
84
111
|
*/
|
|
85
112
|
export function registerGatewayMeetingModels(defaults: GatewayRoutedDefaults, transcription?: TranscriptionRoleSpec): void {
|
|
86
113
|
registerTranscriptionRoute(transcription === undefined ? gatewayTranscriptionRoute() : stackTranscriptionRoute(transcription));
|
|
114
|
+
// Voice clips need the role: the legacy one-model route reads its model from the MEETING policy, and a chat message
|
|
115
|
+
// has no meeting policy. With no role, no transcriber registers, and a clip reaches the agent as unheard.
|
|
116
|
+
if (transcription !== undefined) registerVoiceTranscriber(voiceClipTranscriber(transcription));
|
|
87
117
|
registerMeetingCompletion(gatewayMeetingCompletion(defaults));
|
|
88
118
|
}
|