@mupt-ai/dari-gateway 0.1.0 → 0.2.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/README.md +8 -1
- package/package.json +3 -1
- package/src/bun.ts +4 -0
- package/src/context/routing_context.ts +1 -1
- package/src/index.ts +5 -4
- package/src/ingress/models.ts +1 -1
- package/src/interaction/types.ts +3 -0
- package/src/platform/platform.ts +33 -1
- package/src/providers/pi.ts +102 -28
- package/src/providers/pi_options.ts +273 -443
- package/src/providers/pi_output.ts +1 -0
- package/src/providers/provider_call.ts +24 -5
- package/src/providers/provider_reported_cost.ts +138 -0
- package/src/providers/stream_finalization.ts +1 -1
- package/src/runtime/explicit_model.ts +27 -0
- package/src/runtime/prepare_routed_completion.ts +37 -1
- package/src/runtime/request_tags.ts +34 -0
- package/src/runtime/routed_completion.ts +2 -0
- package/src/runtime/routed_runtime.ts +18 -1
- package/src/self_host/main.ts +7 -2
- package/src/self_host/resolve.ts +5 -5
- package/src/strategies/routing.ts +1 -1
- package/src/telemetry/report.ts +10 -5
- package/src/telemetry/usage.ts +5 -1
- package/src/types.ts +15 -0
package/README.md
CHANGED
|
@@ -111,7 +111,14 @@ const handler = createRequestHandler({ platform, selectorBackend: createPriority
|
|
|
111
111
|
Bun.serve({ port: 8080, fetch: handler, idleTimeout: 0 });
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
`src/self_host/` is the reference platform.
|
|
114
|
+
The root entry needs no Bun APIs, so any runtime with web-standard `fetch`, `Request`, and `Response` can serve the handler. `startServer` and `startSelfHostedGateway` (Bun.serve and bun:sqlite) are in `@mupt-ai/dari-gateway/bun`. `src/self_host/` is the reference platform.
|
|
115
|
+
|
|
116
|
+
Platforms can also:
|
|
117
|
+
|
|
118
|
+
- implement `resolveProviderCredential({ provider, model, routerId, organizationId, apiKey })` to supply a per-request `{ apiKey, baseUrl?, headers? }`, such as a tenant's own key, in place of the router's resolved key;
|
|
119
|
+
- resolve routers with `strict: true` to serve only the requested `dari/model/<id>` at the requested thinking level, with no selector, lease, fallback, or personal subscription;
|
|
120
|
+
- read exact usage from `recordRequest`: the summary carries input, output, cache-read, and cache-write tokens and `total_cost_usd`, preferring the provider's billed cost (`cost_source: "provider"`, from OpenRouter and Vercel AI Gateway). Failed and aborted requests are recorded too;
|
|
121
|
+
- attribute cost with `X-Dari-Tags: run=r1,trial=3`, which is parsed into `RequestRecord.tags` and the `recordUsage` payload's `tags`.
|
|
115
122
|
|
|
116
123
|
## Development
|
|
117
124
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mupt-ai/dari-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Self-hostable LLM gateway for coding agents and APIs: one endpoint for Anthropic Messages, OpenAI Chat Completions, and OpenAI Responses, with subscription pooling, failover, and prompt-cache-aware routing.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./src/index.ts",
|
|
13
|
+
"./bun": "./src/bun.ts",
|
|
13
14
|
"./*": "./src/*.ts"
|
|
14
15
|
},
|
|
15
16
|
"bin": {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"src",
|
|
20
21
|
"!src/**/*.test.ts",
|
|
21
22
|
"!src/**/*.fixtures.ts",
|
|
23
|
+
"!src/**/*.cases.ts",
|
|
22
24
|
"!src/test_support",
|
|
23
25
|
"!src/**/test_support.ts",
|
|
24
26
|
"dari-gateway.example.yaml",
|
package/src/bun.ts
ADDED
|
@@ -122,7 +122,7 @@ export async function buildRoutingContext(args: {
|
|
|
122
122
|
imageInput: args.credentials.selector_image_input_enabled === true,
|
|
123
123
|
});
|
|
124
124
|
const leaseDepth = interactionLeaseFenceDepth(args.interaction);
|
|
125
|
-
const metadataLookup =
|
|
125
|
+
const metadataLookup = modelMetadataLookup(args.credentials.model_configs);
|
|
126
126
|
const targetsByModel = new Map<string, ProviderIdentity>();
|
|
127
127
|
for (const model of configuredCandidateModels) {
|
|
128
128
|
targetsByModel.set(
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
// Public entry points. Every module is also importable by path
|
|
2
2
|
// (`@mupt-ai/dari-gateway/<dir>/<module>`) for deployments that compose the
|
|
3
|
-
// gateway with their own platform.
|
|
3
|
+
// gateway with their own platform. This entry needs no Bun APIs; the Bun
|
|
4
|
+
// server and the SQLite-backed self-hosted gateway live in `./bun`.
|
|
4
5
|
export { createRequestHandler, type GatewayHandlerOptions } from "./http/handler";
|
|
5
|
-
export { installShutdownHandlers, startServer } from "./http/serve";
|
|
6
6
|
export type {
|
|
7
7
|
GatewayPlatform,
|
|
8
8
|
OperationalErrorContext,
|
|
9
9
|
PendingLeaseWrite,
|
|
10
10
|
PrefixStateWrite,
|
|
11
|
+
ProviderCredentialRequest,
|
|
11
12
|
RequestErrorContext,
|
|
12
13
|
RequestRecord,
|
|
13
14
|
RequestSummary,
|
|
15
|
+
RequestTags,
|
|
14
16
|
ResolveRouterRequest,
|
|
15
17
|
SubscriptionHold,
|
|
16
18
|
} from "./platform/platform";
|
|
17
19
|
export { createAutoRoutingSelectorBackend } from "./strategies/auto_routing_selector";
|
|
18
20
|
export { createPrioritySelectorBackend } from "./strategies/priority_selector";
|
|
19
21
|
export { createLlmSelectorBackend, type SelectorBackend } from "./strategies/routing";
|
|
20
|
-
export type { ResolvedRouter } from "./types";
|
|
21
|
-
export { startSelfHostedGateway } from "./self_host/main";
|
|
22
|
+
export type { ProviderCredential, ResolvedRouter } from "./types";
|
package/src/ingress/models.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function handleModels(request: Request, routerId: string): Promise<
|
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
const metadataLookup =
|
|
36
|
+
const metadataLookup = modelMetadataLookup(router.model_configs);
|
|
37
37
|
const imageInputFor = (model: string) => modelSupportsImageInput(metadataLookup(model));
|
|
38
38
|
return Response.json(codexModelsResponse(router, imageInputFor), {
|
|
39
39
|
headers: {
|
package/src/interaction/types.ts
CHANGED
|
@@ -264,6 +264,9 @@ export type InteractionUsage = {
|
|
|
264
264
|
cacheWrite: number;
|
|
265
265
|
total: number;
|
|
266
266
|
};
|
|
267
|
+
// "provider" when cost.total is the provider's billed amount rather than a
|
|
268
|
+
// price from the model's rates.
|
|
269
|
+
costSource?: "provider";
|
|
267
270
|
};
|
|
268
271
|
|
|
269
272
|
export type InteractionOutput = {
|
package/src/platform/platform.ts
CHANGED
|
@@ -6,7 +6,12 @@ import type {
|
|
|
6
6
|
import type { HarnessConversationIdentity } from "../runtime/harness_identity";
|
|
7
7
|
import type { InteractionRequest, SourceProtocol } from "../interaction/types";
|
|
8
8
|
import type { SubscriptionUsageLimit } from "../providers/subscription_limits";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ClientHarness,
|
|
11
|
+
ProviderCredential,
|
|
12
|
+
ProviderKeySource,
|
|
13
|
+
ResolvedRouter,
|
|
14
|
+
} from "../types";
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Everything the gateway runtime needs from the deployment around it: router
|
|
@@ -24,6 +29,15 @@ export type GatewayPlatform = {
|
|
|
24
29
|
* `prefixHashes`. Throws HttpError(401) for an unknown key.
|
|
25
30
|
*/
|
|
26
31
|
resolveRouter(request: ResolveRouterRequest): Promise<ResolvedRouter>;
|
|
32
|
+
/**
|
|
33
|
+
* Optional. Supply the credential for one enabled model of this request,
|
|
34
|
+
* consulted after `resolveRouter` and before its provider keys (for the
|
|
35
|
+
* self-hosted gateway, its environment keys), so an embedder can serve
|
|
36
|
+
* per-tenant keys from its own store. Undefined keeps the resolved key.
|
|
37
|
+
*/
|
|
38
|
+
resolveProviderCredential?(
|
|
39
|
+
request: ProviderCredentialRequest,
|
|
40
|
+
): Promise<ProviderCredential | undefined>;
|
|
27
41
|
/** Persist one completed turn's prefix entry and/or refresh a read entry. */
|
|
28
42
|
writePrefixState(write: PrefixStateWrite): Promise<void>;
|
|
29
43
|
/** Set or clear a conversation's prefetched routing lease. */
|
|
@@ -53,6 +67,15 @@ export type ResolveRouterRequest = {
|
|
|
53
67
|
clientHarness?: ClientHarness;
|
|
54
68
|
};
|
|
55
69
|
|
|
70
|
+
export type ProviderCredentialRequest = {
|
|
71
|
+
provider: Provider;
|
|
72
|
+
model: string;
|
|
73
|
+
routerId: string;
|
|
74
|
+
organizationId: string;
|
|
75
|
+
// The gateway key the request authenticated with.
|
|
76
|
+
apiKey: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
56
79
|
export type PrefixStateWrite = {
|
|
57
80
|
organization_id: string;
|
|
58
81
|
router_id: string;
|
|
@@ -94,8 +117,14 @@ export type RequestRecord = {
|
|
|
94
117
|
isNewConversation: boolean;
|
|
95
118
|
zdr: boolean;
|
|
96
119
|
harnessConversationIdentity?: HarnessConversationIdentity;
|
|
120
|
+
// Parsed from the request's X-Dari-Tags header; empty when absent.
|
|
121
|
+
tags: RequestTags;
|
|
97
122
|
};
|
|
98
123
|
|
|
124
|
+
// Caller-supplied attribution labels (run, trial, setting), opaque to the
|
|
125
|
+
// gateway.
|
|
126
|
+
export type RequestTags = Record<string, string>;
|
|
127
|
+
|
|
99
128
|
export type RequestSummary = {
|
|
100
129
|
request_id: string;
|
|
101
130
|
organization_id: string;
|
|
@@ -130,7 +159,10 @@ export type RequestSummary = {
|
|
|
130
159
|
cache_write_tokens: number | null;
|
|
131
160
|
output_tokens: number | null;
|
|
132
161
|
total_tokens: number | null;
|
|
162
|
+
// Provider-reported cost when the provider returns one (OpenRouter, Vercel
|
|
163
|
+
// AI Gateway), otherwise priced from the model's rates when known.
|
|
133
164
|
total_cost_usd: number | null;
|
|
165
|
+
cost_source: "provider" | "pricing" | null;
|
|
134
166
|
pricing_known: boolean | null;
|
|
135
167
|
};
|
|
136
168
|
|
package/src/providers/pi.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSupportedThinkingLevels,
|
|
3
|
+
normalizeContext,
|
|
4
|
+
type Api,
|
|
5
|
+
type AssistantMessageEventStream,
|
|
6
|
+
type Context,
|
|
7
|
+
type Model,
|
|
8
|
+
type ProviderStreams,
|
|
9
|
+
} from "@mupt-ai/pi-ai";
|
|
10
|
+
import { anthropicMessagesApi } from "@mupt-ai/pi-ai/api/anthropic-messages.lazy";
|
|
11
|
+
import { azureOpenAIResponsesApi } from "@mupt-ai/pi-ai/api/azure-openai-responses.lazy";
|
|
12
|
+
import { bedrockConverseStreamApi } from "@mupt-ai/pi-ai/api/bedrock-converse-stream.lazy";
|
|
13
|
+
import { googleGenerativeAIApi } from "@mupt-ai/pi-ai/api/google-generative-ai.lazy";
|
|
14
|
+
import { googleVertexApi } from "@mupt-ai/pi-ai/api/google-vertex.lazy";
|
|
15
|
+
import { mistralConversationsApi } from "@mupt-ai/pi-ai/api/mistral-conversations.lazy";
|
|
16
|
+
import { openAICodexResponsesApi } from "@mupt-ai/pi-ai/api/openai-codex-responses.lazy";
|
|
17
|
+
import { openAICompletionsApi } from "@mupt-ai/pi-ai/api/openai-completions.lazy";
|
|
18
|
+
import { openAIResponsesApi } from "@mupt-ai/pi-ai/api/openai-responses.lazy";
|
|
19
|
+
import { getBuiltinModel } from "@mupt-ai/pi-ai/providers/all";
|
|
1
20
|
import {
|
|
2
21
|
REASONING_EFFORTS,
|
|
3
22
|
isRecord,
|
|
@@ -22,6 +41,12 @@ import {
|
|
|
22
41
|
messageWithSafeguardResults,
|
|
23
42
|
safeguardResultsTap,
|
|
24
43
|
} from "./anthropic_safeguards";
|
|
44
|
+
import {
|
|
45
|
+
eventsWithProviderReportedCost,
|
|
46
|
+
messageWithProviderReportedCost,
|
|
47
|
+
providerReportedCostTap,
|
|
48
|
+
reportsProviderCost,
|
|
49
|
+
} from "./provider_reported_cost";
|
|
25
50
|
import {
|
|
26
51
|
eventsWithSubscriptionCacheWrites,
|
|
27
52
|
messageWithSubscriptionCacheWrites,
|
|
@@ -43,6 +68,12 @@ export type ModelRuntimeMetadata = {
|
|
|
43
68
|
supportedThinkingLevels: string[];
|
|
44
69
|
};
|
|
45
70
|
|
|
71
|
+
// A platform-supplied endpoint for one call (see ProviderCredential).
|
|
72
|
+
export type ProviderEndpoint = {
|
|
73
|
+
baseUrl?: string;
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
|
|
46
77
|
export type ProviderCompletion = {
|
|
47
78
|
message: Record<string, unknown>;
|
|
48
79
|
source: ProviderIdentity;
|
|
@@ -55,14 +86,40 @@ export type ProviderEventStream = {
|
|
|
55
86
|
|
|
56
87
|
export type ModelMetadataLookup = (modelId: string, pricing?: ModelPricing | null) => ModelRuntimeMetadata;
|
|
57
88
|
|
|
58
|
-
|
|
89
|
+
// Pi's built-in providers stream through these API implementations unchanged.
|
|
90
|
+
// Dari always passes resolved credentials, so it dispatches on the API rather
|
|
91
|
+
// than through a Models collection with its own auth resolution.
|
|
92
|
+
const PI_APIS: Record<string, ProviderStreams> = {
|
|
93
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
94
|
+
"azure-openai-responses": azureOpenAIResponsesApi(),
|
|
95
|
+
"bedrock-converse-stream": bedrockConverseStreamApi(),
|
|
96
|
+
"google-generative-ai": googleGenerativeAIApi(),
|
|
97
|
+
"google-vertex": googleVertexApi(),
|
|
98
|
+
"mistral-conversations": mistralConversationsApi(),
|
|
99
|
+
"openai-codex-responses": openAICodexResponsesApi(),
|
|
100
|
+
"openai-completions": openAICompletionsApi(),
|
|
101
|
+
"openai-responses": openAIResponsesApi(),
|
|
102
|
+
};
|
|
59
103
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
function piStreamSimple(
|
|
105
|
+
model: Record<string, unknown>,
|
|
106
|
+
context: Record<string, unknown>,
|
|
107
|
+
options: Record<string, unknown>,
|
|
108
|
+
): AssistantMessageEventStream {
|
|
109
|
+
const api = PI_APIS[String(model.api)];
|
|
110
|
+
if (!api) throw new Error(`No Pi API implementation for ${String(model.api)}.`);
|
|
111
|
+
return api.streamSimple(model as unknown as Model<Api>, normalizeContext(context as unknown as Context), options);
|
|
64
112
|
}
|
|
65
113
|
|
|
114
|
+
export const pi: PiAi = {
|
|
115
|
+
completeSimple: async (model, context, options) =>
|
|
116
|
+
await piStreamSimple(model, context, options).result() as unknown as Record<string, unknown>,
|
|
117
|
+
streamSimple: (model, context, options) =>
|
|
118
|
+
piStreamSimple(model, context, options) as unknown as AsyncIterable<Record<string, unknown>>,
|
|
119
|
+
getModel: (provider, id) => getBuiltinModel(provider as never, id as never) as unknown as Record<string, unknown> | undefined,
|
|
120
|
+
getSupportedThinkingLevels: (model) => getSupportedThinkingLevels(model as unknown as Model<Api>),
|
|
121
|
+
};
|
|
122
|
+
|
|
66
123
|
export async function completeInteractionModel(args: {
|
|
67
124
|
model: string;
|
|
68
125
|
request: InteractionRequest;
|
|
@@ -70,18 +127,19 @@ export async function completeInteractionModel(args: {
|
|
|
70
127
|
auth?: ResolvedProviderAuth;
|
|
71
128
|
pricing?: ModelPricing | null;
|
|
72
129
|
config?: ResolvedRouterModelConfig;
|
|
130
|
+
endpoint?: ProviderEndpoint;
|
|
73
131
|
sessionId?: string;
|
|
74
132
|
selectedReasoningEffort?: ReasoningEffort;
|
|
75
133
|
timeoutMs?: number;
|
|
76
134
|
signal?: AbortSignal;
|
|
77
135
|
fetch?: ProviderFetch;
|
|
78
136
|
}): Promise<ProviderCompletion> {
|
|
79
|
-
const
|
|
80
|
-
const model = buildModel(pi, args.model, args.pricing, args.config);
|
|
137
|
+
const model = modelAtEndpoint(buildModel(pi, args.model, args.pricing, args.config), args.endpoint);
|
|
81
138
|
const source = providerIdentityFromModel(model);
|
|
82
139
|
const context = buildContext(args.request, source);
|
|
140
|
+
const costTap = reportsProviderCost(model) ? providerReportedCostTap(args.fetch) : undefined;
|
|
83
141
|
const safeguards = forwardsAnthropicSafeguards(args.request, model)
|
|
84
|
-
? safeguardResultsTap(args.fetch)
|
|
142
|
+
? safeguardResultsTap(costTap?.fetch ?? args.fetch)
|
|
85
143
|
: undefined;
|
|
86
144
|
const options = buildOptions(
|
|
87
145
|
args.request,
|
|
@@ -93,16 +151,19 @@ export async function completeInteractionModel(args: {
|
|
|
93
151
|
args.selectedReasoningEffort,
|
|
94
152
|
args.timeoutMs,
|
|
95
153
|
args.signal,
|
|
96
|
-
safeguards?.fetch ?? args.fetch,
|
|
154
|
+
safeguards?.fetch ?? costTap?.fetch ?? args.fetch,
|
|
97
155
|
);
|
|
98
156
|
const usageContext = subscriptionUsageContext(args.model, model, source);
|
|
99
157
|
const message = options.emitProviderEvents === true
|
|
100
158
|
? await completeProviderEventStream(pi.streamSimple(model, context, options))
|
|
101
159
|
: await pi.completeSimple(model, context, options);
|
|
102
160
|
return {
|
|
103
|
-
message:
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
message: messageWithProviderReportedCost(
|
|
162
|
+
messageWithSubscriptionCacheWrites(
|
|
163
|
+
messageWithSafeguardResults(message, safeguards?.results()),
|
|
164
|
+
usageContext
|
|
165
|
+
),
|
|
166
|
+
costTap?.cost(),
|
|
106
167
|
),
|
|
107
168
|
source,
|
|
108
169
|
};
|
|
@@ -167,16 +228,17 @@ export async function streamInteractionModel(args: {
|
|
|
167
228
|
auth?: ResolvedProviderAuth;
|
|
168
229
|
pricing?: ModelPricing | null;
|
|
169
230
|
config?: ResolvedRouterModelConfig;
|
|
231
|
+
endpoint?: ProviderEndpoint;
|
|
170
232
|
sessionId?: string;
|
|
171
233
|
selectedReasoningEffort?: ReasoningEffort;
|
|
172
234
|
signal?: AbortSignal;
|
|
173
235
|
fetch?: ProviderFetch;
|
|
174
236
|
}): Promise<ProviderEventStream> {
|
|
175
|
-
const
|
|
176
|
-
const model = buildModel(pi, args.model, args.pricing, args.config);
|
|
237
|
+
const model = modelAtEndpoint(buildModel(pi, args.model, args.pricing, args.config), args.endpoint);
|
|
177
238
|
const source = providerIdentityFromModel(model);
|
|
239
|
+
const costTap = reportsProviderCost(model) ? providerReportedCostTap(args.fetch) : undefined;
|
|
178
240
|
const safeguards = forwardsAnthropicSafeguards(args.request, model)
|
|
179
|
-
? safeguardResultsTap(args.fetch)
|
|
241
|
+
? safeguardResultsTap(costTap?.fetch ?? args.fetch)
|
|
180
242
|
: undefined;
|
|
181
243
|
const events = pi.streamSimple(
|
|
182
244
|
model,
|
|
@@ -191,20 +253,34 @@ export async function streamInteractionModel(args: {
|
|
|
191
253
|
args.selectedReasoningEffort,
|
|
192
254
|
undefined,
|
|
193
255
|
args.signal,
|
|
194
|
-
safeguards?.fetch ?? args.fetch,
|
|
256
|
+
safeguards?.fetch ?? costTap?.fetch ?? args.fetch,
|
|
195
257
|
)
|
|
196
258
|
);
|
|
259
|
+
const withCacheWrites = eventsWithSubscriptionCacheWrites(
|
|
260
|
+
safeguards ? eventsWithSafeguardResults(events, safeguards) : events,
|
|
261
|
+
subscriptionUsageContext(args.model, model, source)
|
|
262
|
+
);
|
|
197
263
|
return {
|
|
198
|
-
events:
|
|
199
|
-
safeguards ? eventsWithSafeguardResults(events, safeguards) : events,
|
|
200
|
-
subscriptionUsageContext(args.model, model, source)
|
|
201
|
-
),
|
|
264
|
+
events: costTap ? eventsWithProviderReportedCost(withCacheWrites, costTap) : withCacheWrites,
|
|
202
265
|
source,
|
|
203
266
|
};
|
|
204
267
|
}
|
|
205
268
|
|
|
206
|
-
|
|
207
|
-
|
|
269
|
+
function modelAtEndpoint(
|
|
270
|
+
model: Record<string, unknown>,
|
|
271
|
+
endpoint: ProviderEndpoint | undefined,
|
|
272
|
+
): Record<string, unknown> {
|
|
273
|
+
if (endpoint === undefined) return model;
|
|
274
|
+
return {
|
|
275
|
+
...model,
|
|
276
|
+
...(endpoint.baseUrl ? { baseUrl: endpoint.baseUrl } : {}),
|
|
277
|
+
...(endpoint.headers
|
|
278
|
+
? { headers: { ...(isRecord(model.headers) ? model.headers : {}), ...endpoint.headers } }
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function modelContextWindow(modelId: string): number | null {
|
|
208
284
|
const provider = providerForModel(modelId);
|
|
209
285
|
const nativeId = nativeModelId(modelId);
|
|
210
286
|
const registered = resolveModelMetadata(pi, provider, modelId, nativeId);
|
|
@@ -215,11 +291,10 @@ export async function modelContextWindow(modelId: string): Promise<number | null
|
|
|
215
291
|
* Pi's catalog price for a canonical model served by `provider`, in USD per
|
|
216
292
|
* million tokens. Null when Pi does not list the model.
|
|
217
293
|
*/
|
|
218
|
-
export
|
|
294
|
+
export function registeredModelPrice(
|
|
219
295
|
modelId: string,
|
|
220
296
|
provider: string = providerForModel(modelId),
|
|
221
|
-
):
|
|
222
|
-
const pi = await piAi();
|
|
297
|
+
): RouterModelPrice | null {
|
|
223
298
|
const registered = resolveModelMetadata(pi, provider, modelId, nativeModelId(modelId));
|
|
224
299
|
const cost = registered?.cost;
|
|
225
300
|
if (!isRecord(cost) || typeof cost.input !== "number" || typeof cost.output !== "number") {
|
|
@@ -233,10 +308,9 @@ export async function registeredModelPrice(
|
|
|
233
308
|
};
|
|
234
309
|
}
|
|
235
310
|
|
|
236
|
-
export
|
|
311
|
+
export function modelMetadataLookup(
|
|
237
312
|
configs?: Record<string, ResolvedRouterModelConfig>
|
|
238
|
-
):
|
|
239
|
-
const pi = await piAi();
|
|
313
|
+
): ModelMetadataLookup {
|
|
240
314
|
return (modelId: string, pricing?: ModelPricing | null) => {
|
|
241
315
|
const model = buildModel(pi, modelId, pricing, configs?.[modelId]);
|
|
242
316
|
return {
|