@open-everywhere/adapters 0.7.0 → 0.8.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/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/aws/sso.ts
CHANGED
|
@@ -18,8 +18,6 @@ import { makePkce, makeState } from "../oe-oauth/pkce.js";
|
|
|
18
18
|
/** A `fetch`-shaped transport — the DI seam the offline test injects (defaults to global fetch). */
|
|
19
19
|
export type FetchLike = typeof fetch;
|
|
20
20
|
|
|
21
|
-
// Core owns the canonical const; re-exported here so this module's existing consumers keep resolving it.
|
|
22
|
-
export { OE_AWS_SSO_CALLBACK_PATH };
|
|
23
21
|
|
|
24
22
|
/** Default OAuth scopes for the Identity Center link. `openid` is LOAD-BEARING, not decoration: it is
|
|
25
23
|
* what makes the token response carry an id-token, and the id-token's `sub` is the only statement of
|
package/agent/instrumentation.ts
CHANGED
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
// every environment. Without it, eve records content for public conversations and in
|
|
11
11
|
// development. A destination cannot restore what this ceiling removes.
|
|
12
12
|
// - `otlpDestination` (the app's `otlp` slot): the consumer's OTLP collector, from the standard
|
|
13
|
-
// `OTEL_EXPORTER_OTLP_*` variables,
|
|
13
|
+
// `OTEL_EXPORTER_OTLP_*` variables, and Vercel's OTel collector, from the `VERCEL_OTEL_ENDPOINTS*`
|
|
14
|
+
// variables the platform sets, as one eve destination. With neither, it is disabled.
|
|
14
15
|
//
|
|
15
16
|
// `scripts/instrumentation-hosted-boot-test.ts` boots both slots through eve's registration path
|
|
16
17
|
// with the env Vercel creates.
|
|
17
18
|
|
|
18
19
|
import { disableInstrumentation } from "eve/instrumentation";
|
|
19
|
-
import { otel, otelIntegration, type OtelDeclaration, type SpanExporter } from "eve/instrumentation/otel";
|
|
20
|
+
import { otel, otelIntegration, type OtelDeclaration, type SpanExporter, type SpanProcessor } from "eve/instrumentation/otel";
|
|
20
21
|
import { OTLPHttpJsonTraceExporter, OTLPHttpProtoTraceExporter } from "@vercel/otel";
|
|
22
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
21
23
|
import { installLogShipping, initTelemetry } from "@open-everywhere/core/agent/adapter-api.js";
|
|
22
24
|
|
|
23
25
|
/** Metadata only: emit the span, record neither content direction. */
|
|
@@ -95,7 +97,68 @@ function otlpExporter(target: OtlpTarget): SpanExporter {
|
|
|
95
97
|
return target.protocol === "http/json" ? new OTLPHttpJsonTraceExporter(config) : new OTLPHttpProtoTraceExporter(config);
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Vercel's OTel collector (OPE-2358). On Vercel, an OpenTelemetry integration from the Vercel
|
|
102
|
+
* Marketplace receives spans through a collector on the function's own host. The platform names it
|
|
103
|
+
* with `VERCEL_OTEL_ENDPOINTS`, `VERCEL_OTEL_ENDPOINTS_PORT` and `VERCEL_OTEL_ENDPOINTS_PROTOCOL`.
|
|
104
|
+
* `registerOTel`'s `"auto"` span processor exported to it; eve's pipeline passes no `"auto"`, so
|
|
105
|
+
* this destination replaces it. The URL shape, the port default and the protocol rule are
|
|
106
|
+
* `@vercel/otel`'s own: `http/protobuf` selects protobuf and every other value selects JSON.
|
|
107
|
+
* `undefined` when the platform set no collector.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveVercelCollectorTarget(env: NodeJS.ProcessEnv = process.env): OtlpTarget | undefined {
|
|
110
|
+
if (!env.VERCEL_OTEL_ENDPOINTS) return undefined;
|
|
111
|
+
const port = env.VERCEL_OTEL_ENDPOINTS_PORT || "4318";
|
|
112
|
+
const protocol: OtlpProtocol = (env.VERCEL_OTEL_ENDPOINTS_PROTOCOL || "http/protobuf") === "http/protobuf" ? "http/protobuf" : "http/json";
|
|
113
|
+
return { url: `http://localhost:${port}/v1/traces`, protocol, headers: {} };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The key under which the Vercel runtime publishes the current request's context. */
|
|
117
|
+
const VERCEL_REQUEST_CONTEXT = Symbol.for("@vercel/request-context");
|
|
118
|
+
|
|
119
|
+
interface VercelRequestContext {
|
|
120
|
+
readonly telemetry?: { readonly traceDrains?: readonly unknown[] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* True when the current Vercel request reports trace drains. The runtime then delivers spans to
|
|
125
|
+
* those drains itself, from eve's Agent Runs export (`telemetry.reportSpans`), so the collector
|
|
126
|
+
* would deliver every span a second time. `@vercel/otel` skips its collector on the same signal.
|
|
127
|
+
* An app that turns Agent Runs off then sends nothing to its trace drains; this destination does not
|
|
128
|
+
* cover that case.
|
|
129
|
+
*/
|
|
130
|
+
export function vercelTraceDrainsConfigured(): boolean {
|
|
131
|
+
const store = (globalThis as Record<symbol, { get?: () => VercelRequestContext | undefined } | undefined>)[VERCEL_REQUEST_CONTEXT];
|
|
132
|
+
return (store?.get?.()?.telemetry?.traceDrains?.length ?? 0) > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Wrap the collector's processor so that it skips every span while trace drains are configured. */
|
|
136
|
+
function skipWhenTraceDrains(inner: BatchSpanProcessor): SpanProcessor {
|
|
137
|
+
return {
|
|
138
|
+
onStart: (span, context) => {
|
|
139
|
+
if (!vercelTraceDrainsConfigured()) inner.onStart(span as never, context as never);
|
|
140
|
+
},
|
|
141
|
+
onEnd: (span) => {
|
|
142
|
+
if (!vercelTraceDrainsConfigured()) inner.onEnd(span as never);
|
|
143
|
+
},
|
|
144
|
+
forceFlush: () => inner.forceFlush(),
|
|
145
|
+
shutdown: () => inner.shutdown(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
const OTLP_TARGET = resolveOtlpTarget();
|
|
150
|
+
const VERCEL_COLLECTOR_TARGET = resolveVercelCollectorTarget();
|
|
99
151
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The consumer's OTLP collector and Vercel's OTel collector, as ONE eve destination: the `otlp` slot
|
|
154
|
+
* file every app already has carries both, so a scaffolded app needs no new file. Disabled when
|
|
155
|
+
* neither is configured. Both obey the `otel` slot's trace policy.
|
|
156
|
+
*/
|
|
157
|
+
export const otlpDestination =
|
|
158
|
+
OTLP_TARGET === undefined && VERCEL_COLLECTOR_TARGET === undefined
|
|
159
|
+
? disableInstrumentation()
|
|
160
|
+
: otelIntegration({
|
|
161
|
+
...(OTLP_TARGET === undefined ? {} : { traceExporter: otlpExporter(OTLP_TARGET) }),
|
|
162
|
+
spanProcessors:
|
|
163
|
+
VERCEL_COLLECTOR_TARGET === undefined ? [] : [skipWhenTraceDrains(new BatchSpanProcessor(otlpExporter(VERCEL_COLLECTOR_TARGET) as never))],
|
|
164
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Gateway transport for the approval judge — core owns the seam; this owns the vendor call on the fixed
|
|
2
2
|
// `judge` model role. NO scope routing: the judge must not inherit whatever a domain routed its seat onto.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { generateText } from "ai";
|
|
5
5
|
import {
|
|
6
6
|
type ApprovalJudgeCompletion,
|
|
7
7
|
type JudgePass,
|
|
8
8
|
registerApprovalJudgeCompletion,
|
|
9
9
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
10
|
-
import { applyGatewayOptions, type GatewayRoutedDefaults } from "./gateway.js";
|
|
10
|
+
import { applyGatewayOptions, gatewayLanguageModel, type GatewayRoutedDefaults } from "./gateway.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Output budget for the scrutiny pass. A reasoning model spends reasoning tokens INSIDE the budget, so a
|
|
@@ -37,7 +37,7 @@ function gatewayApprovalJudgeCompletion(defaults: GatewayRoutedDefaults): Approv
|
|
|
37
37
|
return async (prompt, pass) => {
|
|
38
38
|
const s = approvalJudgeCallSettings(pass, defaults);
|
|
39
39
|
const { text } = await generateText({
|
|
40
|
-
model:
|
|
40
|
+
model: gatewayLanguageModel(s.id, defaults.via),
|
|
41
41
|
prompt,
|
|
42
42
|
...(s.maxOutputTokens !== undefined ? { maxOutputTokens: s.maxOutputTokens } : {}),
|
|
43
43
|
...(s.providerOptions !== undefined
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Gateway transport for the dream judge — core owns the seam; this owns one completion on the `utility`
|
|
2
2
|
// role. The SCOPE locates the route: a channel key, a silo's owning domain, else `org`. It FAILS QUIET.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { generateText } from "ai";
|
|
5
5
|
import {
|
|
6
6
|
type ModelRoute,
|
|
7
7
|
modelRouteFor,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
registerFailureFacetJudge,
|
|
17
17
|
stampModelCall,
|
|
18
18
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
19
|
-
import { applyGatewayOptions, type GatewayRoutedDefaults } from "./gateway.js";
|
|
19
|
+
import { applyGatewayOptions, gatewayLanguageModel, type GatewayRoutedDefaults } from "./gateway.js";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* The effective `utility` route for a dream scope (channel key | silo name | other).
|
|
@@ -47,10 +47,12 @@ export function utilityRouteForScope(scope: string): ModelRoute {
|
|
|
47
47
|
export function dreamCallSettings(
|
|
48
48
|
route: ModelRoute,
|
|
49
49
|
defaults: GatewayRoutedDefaults,
|
|
50
|
-
): { id: string; reasoning?: ModelRoute["reasoning"]; providerOptions?: Record<string, unknown> } {
|
|
50
|
+
): { id: string; via?: string; reasoning?: ModelRoute["reasoning"]; providerOptions?: Record<string, unknown> } {
|
|
51
51
|
const id = route.id ?? defaults.id;
|
|
52
52
|
const options = applyGatewayOptions(route, {}, { id, defaultId: defaults.id }, defaults.gateway);
|
|
53
|
-
|
|
53
|
+
// The route's `via` wins over the role's (OPE-2556); absent both, the adapter uses the `gateway` target.
|
|
54
|
+
const via = route.via ?? defaults.via;
|
|
55
|
+
return { id, ...(via !== undefined ? { via } : {}), reasoning: route.reasoning, providerOptions: options.providerOptions };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -60,7 +62,7 @@ export function dreamCallSettings(
|
|
|
60
62
|
export function resolvedDreamCallSettings(
|
|
61
63
|
scope: string,
|
|
62
64
|
defaults: GatewayRoutedDefaults,
|
|
63
|
-
): { id: string; reasoning?: ModelRoute["reasoning"]; providerOptions?: Record<string, unknown> } {
|
|
65
|
+
): { id: string; via?: string; reasoning?: ModelRoute["reasoning"]; providerOptions?: Record<string, unknown> } {
|
|
64
66
|
const s = dreamCallSettings(utilityRouteForScope(scope), defaults);
|
|
65
67
|
stampModelCall(s.id);
|
|
66
68
|
return s;
|
|
@@ -71,7 +73,7 @@ function gatewayDreamCompletion(defaults: GatewayRoutedDefaults): DreamCompletio
|
|
|
71
73
|
return async (scope, prompt) => {
|
|
72
74
|
const s = resolvedDreamCallSettings(scope, defaults);
|
|
73
75
|
const { text } = await generateText({
|
|
74
|
-
model:
|
|
76
|
+
model: gatewayLanguageModel(s.id, s.via),
|
|
75
77
|
prompt,
|
|
76
78
|
...(s.reasoning !== undefined ? { reasoning: s.reasoning } : {}),
|
|
77
79
|
...(s.providerOptions !== undefined
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
// ② REACHABILITY, from one GET of the gateway's PUBLIC model catalog — the same unauthenticated
|
|
13
13
|
// endpoint `scripts/seed-model-catalog-cache.ts` reads.
|
|
14
14
|
//
|
|
15
|
+
// ③ THE MODEL CONFIG (OPE-2556), when the app registered its models: one GET of the public
|
|
16
|
+
// `/v1/models/<id>/endpoints` read per model, judged against `models.targets` and `models.dataPolicy`
|
|
17
|
+
// (model-config-check.ts). A demand no provider can meet turns the row "no". The route is public, so the
|
|
18
|
+
// detail names no model, provider or target, and counts nothing. The answer is cached for ten minutes, so an
|
|
19
|
+
// anonymous caller cannot make the route fan out a read per request.
|
|
20
|
+
//
|
|
15
21
|
// It does NOT prove the deployment's credential is ACCEPTED: the only call that proves that is an
|
|
16
22
|
// inference call, which is neither cheap nor free, and a status route an unbounded caller can poke
|
|
17
23
|
// must not be able to spend money. The detail string says so in as many words, because a status
|
|
@@ -20,7 +26,17 @@
|
|
|
20
26
|
// FAIL-SOFT and BOUNDED: a 2-second abort, and any throw answers "no" with no vendor error text
|
|
21
27
|
// forwarded (a fetch failure's message carries the host, and the status route is public).
|
|
22
28
|
|
|
23
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_MODEL_VIA,
|
|
31
|
+
getSettings,
|
|
32
|
+
modelTargetFor,
|
|
33
|
+
registerStatusProbe,
|
|
34
|
+
onVercel,
|
|
35
|
+
type ProbeAnswer,
|
|
36
|
+
type ResolvableModel,
|
|
37
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
38
|
+
import { checkModelConfig, type ConfigCheckResult } from "./model-config-check.js";
|
|
39
|
+
import { targetBaseUrl } from "./target-url.js";
|
|
24
40
|
|
|
25
41
|
/** The gateway's PUBLIC, unauthenticated model catalog — eve's own `AI_GATEWAY_MODELS_CATALOG_URL`. */
|
|
26
42
|
export const GATEWAY_CATALOG_URL = "https://ai-gateway.vercel.sh/v1/models/catalog";
|
|
@@ -28,13 +44,19 @@ export const GATEWAY_CATALOG_URL = "https://ai-gateway.vercel.sh/v1/models/catal
|
|
|
28
44
|
/** How long the probe waits before it calls the gateway unreachable. */
|
|
29
45
|
export const GATEWAY_PROBE_TIMEOUT_MS = 2_000;
|
|
30
46
|
|
|
47
|
+
/** The `gateway` target's base URL: `models.targets.gateway.url`, else the deprecated `OE_AI_GATEWAY_URL` in `env`. */
|
|
48
|
+
function gatewayBaseUrl(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
49
|
+
const targets = getSettings().models.targets;
|
|
50
|
+
return targetBaseUrl(DEFAULT_MODEL_VIA, modelTargetFor(DEFAULT_MODEL_VIA, targets), env);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/**
|
|
32
|
-
* The catalog URL this deployment probes. `
|
|
33
|
-
* an on-prem proxy, a fixture), and it names the CHAT base (`…/v4/ai`), so the
|
|
34
|
-
* from its ORIGIN rather than appended to it.
|
|
54
|
+
* The catalog URL this deployment probes. The `gateway` target's url (or the deprecated `OE_AI_GATEWAY_URL`)
|
|
55
|
+
* retargets the whole gateway (a relay, an on-prem proxy, a fixture), and it names the CHAT base (`…/v4/ai`), so the
|
|
56
|
+
* catalog is taken from its ORIGIN rather than appended to it.
|
|
35
57
|
*/
|
|
36
58
|
export function gatewayCatalogUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
37
|
-
const override = env
|
|
59
|
+
const override = gatewayBaseUrl(env);
|
|
38
60
|
if (!override) return GATEWAY_CATALOG_URL;
|
|
39
61
|
try {
|
|
40
62
|
return `${new URL(override).origin}/v1/models/catalog`;
|
|
@@ -49,6 +71,65 @@ export interface GatewayProbeDeps {
|
|
|
49
71
|
readonly env?: NodeJS.ProcessEnv;
|
|
50
72
|
readonly fetchImpl?: typeof globalThis.fetch;
|
|
51
73
|
readonly vercel?: boolean;
|
|
74
|
+
/** The models to run the config check on. Absent: the registered list, else no config check. */
|
|
75
|
+
readonly models?: () => readonly ResolvableModel[];
|
|
76
|
+
/** The config-check cache. Absent: the module's own. */
|
|
77
|
+
readonly cache?: ConfigCheckCache;
|
|
78
|
+
readonly now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** How long one config-check answer is reused. */
|
|
82
|
+
const MODEL_CONFIG_CHECK_TTL_MS = 10 * 60_000;
|
|
83
|
+
|
|
84
|
+
/** A one-entry cache of the last config-check answer. */
|
|
85
|
+
export interface ConfigCheckCache {
|
|
86
|
+
value?: { readonly at: number; readonly result: ConfigCheckResult };
|
|
87
|
+
}
|
|
88
|
+
const CONFIG_CHECK_CACHE: ConfigCheckCache = {};
|
|
89
|
+
|
|
90
|
+
let registeredModels: (() => readonly ResolvableModel[]) | undefined;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register the models the deployment can resolve, so the `model-route` probe also runs the live config check.
|
|
94
|
+
* Called once from the app's boot wiring, beside the other registrations.
|
|
95
|
+
*/
|
|
96
|
+
export function registerModelConfigCheck(models: () => readonly ResolvableModel[]): void {
|
|
97
|
+
registeredModels = models;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The config-check answer in closed-vocabulary words, for the PUBLIC status route. Pure. */
|
|
101
|
+
function publicConfigWords(r: ConfigCheckResult): string {
|
|
102
|
+
if (r.met === "no") return "the model config has a models.dataPolicy, providers.only or target dataPolicy demand that no provider can serve for at least one model — run `npm run check:model-config` for the names";
|
|
103
|
+
// An unused `providers.only` name is a warning on a config that was fully measured: say so, not "not measured".
|
|
104
|
+
if (r.met === "unknown" && r.findings.every((f) => f.row.endsWith(".providers.only"))) {
|
|
105
|
+
return "the model config can be served; a providers.only name serves none of the checked models — run `npm run check:model-config` for the names";
|
|
106
|
+
}
|
|
107
|
+
if (r.met === "unknown") return "the model config was not fully measured — run `npm run check:model-config` for the details";
|
|
108
|
+
return "the model config can be served";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Run the config check, or reuse an answer younger than {@link MODEL_CONFIG_CHECK_TTL_MS}. Never throws. */
|
|
112
|
+
async function cachedConfigCheck(deps: GatewayProbeDeps, models: () => readonly ResolvableModel[]): Promise<ConfigCheckResult> {
|
|
113
|
+
const cache = deps.cache ?? CONFIG_CHECK_CACHE;
|
|
114
|
+
const now = (deps.now ?? Date.now)();
|
|
115
|
+
if (cache.value !== undefined && now - cache.value.at < MODEL_CONFIG_CHECK_TTL_MS) return cache.value.result;
|
|
116
|
+
const env = deps.env ?? process.env;
|
|
117
|
+
let result: ConfigCheckResult;
|
|
118
|
+
try {
|
|
119
|
+
const settings = getSettings().models;
|
|
120
|
+
result = await checkModelConfig({
|
|
121
|
+
models: models(),
|
|
122
|
+
targets: settings.targets,
|
|
123
|
+
policy: settings.dataPolicy,
|
|
124
|
+
baseUrlFor: (via) => targetBaseUrl(via, modelTargetFor(via, settings.targets), env),
|
|
125
|
+
timeoutMs: GATEWAY_PROBE_TIMEOUT_MS,
|
|
126
|
+
...(deps.fetchImpl !== undefined ? { fetchImpl: deps.fetchImpl } : {}),
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
result = { met: "unknown", findings: [{ met: "unknown", row: "config", detail: "the model config could not be read" }], checked: 0 };
|
|
130
|
+
}
|
|
131
|
+
cache.value = { at: now, result };
|
|
132
|
+
return result;
|
|
52
133
|
}
|
|
53
134
|
|
|
54
135
|
/** Run the probe once. Exported so a test drives every arm with no network. */
|
|
@@ -61,10 +142,10 @@ export async function probeGatewayRoute(deps: GatewayProbeDeps = {}): Promise<Pr
|
|
|
61
142
|
// owes nobody a model catalog and may need no key of ours. Both of this probe's facts are then
|
|
62
143
|
// about the shipped Vercel gateway, not about the route the deployment actually uses — and
|
|
63
144
|
// "measured, and it does not hold" would be false for a deployment whose models route fine.
|
|
64
|
-
const retargeted = (env
|
|
145
|
+
const retargeted = gatewayBaseUrl(env) !== undefined;
|
|
65
146
|
if (!vercel && !keySet) {
|
|
66
147
|
if (retargeted) {
|
|
67
|
-
return { met: "unknown", detail: "the model route is retargeted
|
|
148
|
+
return { met: "unknown", detail: "the model route is retargeted (models.targets.gateway.url) and holds no AI_GATEWAY_API_KEY — a relay may need neither, so this was not measured" };
|
|
68
149
|
}
|
|
69
150
|
return { met: "no", detail: "no gateway credential: off Vercel the deployment needs AI_GATEWAY_API_KEY (on Vercel, OIDC authenticates it)" };
|
|
70
151
|
}
|
|
@@ -83,10 +164,15 @@ export async function probeGatewayRoute(deps: GatewayProbeDeps = {}): Promise<Pr
|
|
|
83
164
|
}
|
|
84
165
|
return { met: "no", detail: `the model gateway answered HTTP ${res.status} to a catalog read; the credential posture is ${credential}` };
|
|
85
166
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
167
|
+
const reached = `the model gateway answered a catalog read and the credential posture is ${credential} — this does not prove an inference call is authorised`;
|
|
168
|
+
const models = deps.models ?? registeredModels;
|
|
169
|
+
if (models === undefined) return { met: "yes", detail: reached };
|
|
170
|
+
// ③ The config check. A demand no provider can meet makes the row "no": every call of that model would fail closed.
|
|
171
|
+
// An unknown part keeps "yes", because the route itself was measured, and the detail says what was not.
|
|
172
|
+
// 🔴 THE STATUS ROUTE IS PUBLIC, so the detail is closed-vocabulary prose: no model id, no provider slug, no target
|
|
173
|
+
// name and no count. `npm run check:model-config` names them, from the app.
|
|
174
|
+
const check = await cachedConfigCheck(deps, models);
|
|
175
|
+
return { met: check.met === "no" ? "no" : "yes", detail: `${reached}; ${publicConfigWords(check)}` };
|
|
90
176
|
} catch {
|
|
91
177
|
return { met: "no", detail: `the model gateway did not answer within ${GATEWAY_PROBE_TIMEOUT_MS} ms; the credential posture is ${credential}` };
|
|
92
178
|
} finally {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// Gateway transport for the image describer — core owns the seam.
|
|
2
|
-
//
|
|
1
|
+
// Gateway transport for the image describer — core owns the seam. The model comes from gateway.ts, so the call
|
|
2
|
+
// carries `models.dataPolicy` (OPE-2535); a gateway's own org policy applies on top. The call is fail-closed,
|
|
3
|
+
// reports its own spend, and must NOT stamp the model.
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { generateText } from "ai";
|
|
6
|
+
import { gatewayLanguageModel } from "./gateway.js";
|
|
5
7
|
import {
|
|
6
8
|
type ImageDescribeRequest,
|
|
7
9
|
type ImageDescribeResult,
|
|
@@ -14,9 +16,11 @@ import {
|
|
|
14
16
|
export interface GatewayVisionDefaults {
|
|
15
17
|
/** Gateway vision model id, e.g. "alibaba/qwen3-vl-instruct". */
|
|
16
18
|
readonly id: string;
|
|
19
|
+
/** The `models.targets` entry the call goes through (OPE-2556). Absent: `gateway`. */
|
|
20
|
+
readonly via?: string;
|
|
17
21
|
/**
|
|
18
|
-
* providerOptions.gateway for the call, forwarded verbatim. Absent in the reference config
|
|
19
|
-
*
|
|
22
|
+
* providerOptions.gateway for the call, forwarded verbatim. Absent in the reference config. The deployment's
|
|
23
|
+
* `models.dataPolicy` rows ride the call whatever this holds, and win over it.
|
|
20
24
|
*/
|
|
21
25
|
readonly gateway?: Record<string, unknown>;
|
|
22
26
|
}
|
|
@@ -128,7 +132,7 @@ export function gatewayImageDescriber(defaults: GatewayVisionDefaults): ImageDes
|
|
|
128
132
|
let res: { text: string } & VisionCallTelemetry;
|
|
129
133
|
try {
|
|
130
134
|
res = await generateText({
|
|
131
|
-
model:
|
|
135
|
+
model: gatewayLanguageModel(defaults.id, defaults.via),
|
|
132
136
|
messages: [
|
|
133
137
|
{
|
|
134
138
|
role: "user",
|