@mupt-ai/dari-gateway 0.2.1 → 0.3.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/README.md +8 -4
- package/dist/context/pricing.js +5 -1
- package/dist/http/handler.js +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/ingress/chat_completions.js +8 -1
- package/dist/ingress/codex.js +4 -0
- package/dist/ingress/models.js +13 -1
- package/dist/ingress/muse_code.d.ts +2 -0
- package/dist/ingress/muse_code.js +8 -0
- package/dist/ingress/muse_models.d.ts +9 -0
- package/dist/ingress/muse_models.js +32 -0
- package/dist/interaction/types.d.ts +1 -0
- package/dist/models/catalog.d.ts +35 -0
- package/dist/models/catalog.js +232 -0
- package/dist/models/execution_provider.d.ts +2 -1
- package/dist/models/execution_provider.js +5 -1
- package/dist/models/provider_defaults.js +3 -0
- package/dist/models/runtime_overrides.d.ts +3 -0
- package/dist/models/runtime_overrides.js +17 -0
- package/dist/models/subscriptions.d.ts +8 -0
- package/dist/models/subscriptions.js +37 -0
- package/dist/platform/platform.d.ts +5 -1
- package/dist/protocol/input/chat_completions.js +3 -1
- package/dist/protocol/input/chat_completions_validation.js +2 -1
- package/dist/providers/pi.js +12 -6
- package/dist/providers/pi_options.js +13 -1
- package/dist/providers/provider_call.js +7 -3
- package/dist/providers/provider_reported_cost.js +17 -2
- package/dist/providers/subscription_limits.d.ts +4 -2
- package/dist/providers/subscription_limits.js +19 -3
- package/dist/runtime/explicit_model.d.ts +3 -0
- package/dist/runtime/explicit_model.js +10 -6
- package/dist/runtime/prepare_routed_completion.js +24 -5
- package/dist/runtime/strict_provider_access.d.ts +6 -0
- package/dist/runtime/strict_provider_access.js +40 -0
- package/dist/self_host/resolve.js +11 -15
- package/dist/self_host/subscriptions/oauth.d.ts +10 -6
- package/dist/self_host/subscriptions/oauth.js +101 -6
- package/dist/self_host/subscriptions/store.js +1 -0
- package/dist/types.d.ts +4 -1
- package/package.json +2 -2
- package/src/context/pricing.ts +5 -2
- package/src/http/handler.ts +7 -0
- package/src/index.ts +9 -1
- package/src/ingress/chat_completions.ts +8 -1
- package/src/ingress/codex.ts +4 -0
- package/src/ingress/models.ts +13 -1
- package/src/ingress/muse_code.ts +9 -0
- package/src/ingress/muse_models.ts +36 -0
- package/src/interaction/types.ts +2 -0
- package/src/models/catalog.ts +310 -0
- package/src/models/execution_provider.ts +4 -1
- package/src/models/provider_defaults.ts +3 -0
- package/src/models/runtime_overrides.ts +22 -0
- package/src/models/subscriptions.ts +54 -0
- package/src/platform/platform.ts +5 -1
- package/src/protocol/input/chat_completions.ts +3 -1
- package/src/protocol/input/chat_completions_validation.ts +2 -1
- package/src/providers/pi.ts +21 -6
- package/src/providers/pi_options.ts +17 -1
- package/src/providers/provider_call.ts +7 -3
- package/src/providers/provider_reported_cost.ts +16 -2
- package/src/providers/subscription_limits.ts +21 -4
- package/src/runtime/explicit_model.ts +15 -16
- package/src/runtime/prepare_routed_completion.ts +22 -3
- package/src/runtime/strict_provider_access.ts +42 -0
- package/src/self_host/resolve.ts +12 -26
- package/src/self_host/subscriptions/oauth.ts +139 -8
- package/src/self_host/subscriptions/store.ts +1 -0
- package/src/types.ts +20 -3
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { getSupportedThinkingLevels, type Api, type Model } from "@mupt-ai/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
getBuiltinModelDataGeneratedAt,
|
|
4
|
+
getBuiltinModels,
|
|
5
|
+
getBuiltinProviders,
|
|
6
|
+
} from "@mupt-ai/pi-ai/providers/all";
|
|
7
|
+
import {
|
|
8
|
+
REASONING_EFFORTS,
|
|
9
|
+
isReasoningEffort,
|
|
10
|
+
isRecord,
|
|
11
|
+
type ReasoningEffort,
|
|
12
|
+
} from "@mupt-ai/dari-router/policy-engine";
|
|
13
|
+
import { HttpError } from "../http/errors.js";
|
|
14
|
+
|
|
15
|
+
// The models each provider can serve to a coding agent, with the limits,
|
|
16
|
+
// thinking levels, and list prices a caller needs to pick and price one.
|
|
17
|
+
// Gateways (OpenRouter, Vercel AI Gateway) are listed live from their public
|
|
18
|
+
// models endpoints; model providers come from the bundled Pi catalog.
|
|
19
|
+
|
|
20
|
+
export type CatalogModel = {
|
|
21
|
+
// Canonical `provider/native-id`; request it as `dari/model/<id>`.
|
|
22
|
+
id: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
native_model_id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
context_window: number | null;
|
|
27
|
+
max_output_tokens: number | null;
|
|
28
|
+
input_modalities: string[];
|
|
29
|
+
// Accepted thinking levels; ["off"] for models that do not reason.
|
|
30
|
+
thinking_levels: ReasoningEffort[];
|
|
31
|
+
// List price in USD per million tokens; null when the source publishes
|
|
32
|
+
// none, e.g. meta-routers whose price depends on the model they pick.
|
|
33
|
+
price: CatalogModelPrice | null;
|
|
34
|
+
// When the price was read: the fetch time for gateways, the catalog's
|
|
35
|
+
// generation time for model providers.
|
|
36
|
+
price_as_of: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type CatalogModelPrice = {
|
|
40
|
+
input: number;
|
|
41
|
+
output: number;
|
|
42
|
+
cached_input: number | null;
|
|
43
|
+
cache_write: number | null;
|
|
44
|
+
// Long-context rates, each applying to prompts above its token threshold.
|
|
45
|
+
tiers: CatalogPriceTier[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type CatalogPriceTier = {
|
|
49
|
+
input_tokens_above: number;
|
|
50
|
+
input: number;
|
|
51
|
+
output: number;
|
|
52
|
+
cached_input: number | null;
|
|
53
|
+
cache_write: number | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type ModelCatalogOptions = {
|
|
57
|
+
// Providers to list; defaults to DEFAULT_CATALOG_PROVIDERS. Any Pi built-in
|
|
58
|
+
// provider id is accepted.
|
|
59
|
+
providers?: readonly string[] | undefined;
|
|
60
|
+
fetch?: ((url: string, init?: RequestInit) => Promise<Response>) | undefined;
|
|
61
|
+
signal?: AbortSignal | undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const DEFAULT_CATALOG_PROVIDERS: readonly string[] = [
|
|
65
|
+
"openai",
|
|
66
|
+
"anthropic",
|
|
67
|
+
"meta",
|
|
68
|
+
"xai",
|
|
69
|
+
"fireworks",
|
|
70
|
+
"openrouter",
|
|
71
|
+
"vercel-ai-gateway",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
type LiveListing = {
|
|
75
|
+
url: string;
|
|
76
|
+
models: (data: unknown[], asOf: string) => CatalogModel[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const LIVE_LISTINGS: Record<string, LiveListing> = {
|
|
80
|
+
openrouter: { url: "https://openrouter.ai/api/v1/models", models: openRouterModels },
|
|
81
|
+
"vercel-ai-gateway": { url: "https://ai-gateway.vercel.sh/v1/models", models: vercelModels },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** List the agent-capable models of each requested provider. */
|
|
85
|
+
export async function listModelCatalog(options: ModelCatalogOptions = {}): Promise<CatalogModel[]> {
|
|
86
|
+
const providers = [...new Set(options.providers ?? DEFAULT_CATALOG_PROVIDERS)];
|
|
87
|
+
const builtin = new Set<string>(getBuiltinProviders());
|
|
88
|
+
for (const provider of providers) {
|
|
89
|
+
if (LIVE_LISTINGS[provider] === undefined && !builtin.has(provider)) {
|
|
90
|
+
throw new HttpError(400, `Unknown catalog provider '${provider}'.`, "unknown_provider", "provider");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const lists = await Promise.all(providers.map(async (provider) => {
|
|
94
|
+
const listing = LIVE_LISTINGS[provider];
|
|
95
|
+
return listing === undefined
|
|
96
|
+
? piModels(provider)
|
|
97
|
+
: listing.models(await fetchListing(listing.url, options), new Date().toISOString());
|
|
98
|
+
}));
|
|
99
|
+
return lists.flat();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchListing(url: string, options: ModelCatalogOptions): Promise<unknown[]> {
|
|
103
|
+
let status: number | undefined;
|
|
104
|
+
let body: unknown = null;
|
|
105
|
+
// Network errors, aborts, and unparseable bodies are upstream failures
|
|
106
|
+
// too: all of them surface as catalog_unavailable rather than a raw 500.
|
|
107
|
+
try {
|
|
108
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
109
|
+
headers: { accept: "application/json" },
|
|
110
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
111
|
+
});
|
|
112
|
+
status = response.status;
|
|
113
|
+
body = response.ok ? await response.json() : null;
|
|
114
|
+
} catch {
|
|
115
|
+
// Leaves body null.
|
|
116
|
+
}
|
|
117
|
+
if (!isRecord(body) || !Array.isArray(body.data)) {
|
|
118
|
+
const detail = status === undefined ? "could not be reached" : `returned no model list (HTTP ${status})`;
|
|
119
|
+
throw new HttpError(502, `${url} ${detail}.`, "catalog_unavailable");
|
|
120
|
+
}
|
|
121
|
+
return body.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function piModels(provider: string): CatalogModel[] {
|
|
125
|
+
const generatedAt = getBuiltinModelDataGeneratedAt();
|
|
126
|
+
const asOf = generatedAt === undefined ? "" : new Date(generatedAt).toISOString();
|
|
127
|
+
const models = getBuiltinModels(provider as never) as unknown as Model<Api>[];
|
|
128
|
+
return models.map((model) => ({
|
|
129
|
+
id: `${provider}/${model.id}`,
|
|
130
|
+
provider,
|
|
131
|
+
native_model_id: model.id,
|
|
132
|
+
name: model.name,
|
|
133
|
+
context_window: model.contextWindow,
|
|
134
|
+
max_output_tokens: model.maxTokens,
|
|
135
|
+
input_modalities: [...model.input],
|
|
136
|
+
thinking_levels: orderedEfforts(getSupportedThinkingLevels(model)),
|
|
137
|
+
price: piPrice(model.cost),
|
|
138
|
+
price_as_of: asOf,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function piPrice(cost: Model<Api>["cost"]): CatalogModelPrice {
|
|
143
|
+
return {
|
|
144
|
+
input: cost.input,
|
|
145
|
+
output: cost.output,
|
|
146
|
+
cached_input: cost.cacheRead,
|
|
147
|
+
cache_write: cost.cacheWrite,
|
|
148
|
+
tiers: (cost.tiers ?? []).map((tier) => ({
|
|
149
|
+
input_tokens_above: tier.inputTokensAbove,
|
|
150
|
+
input: tier.input,
|
|
151
|
+
output: tier.output,
|
|
152
|
+
cached_input: tier.cacheRead,
|
|
153
|
+
cache_write: tier.cacheWrite,
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// https://openrouter.ai/docs/api/api-reference/models/get-models
|
|
159
|
+
function openRouterModels(data: unknown[], asOf: string): CatalogModel[] {
|
|
160
|
+
return data.filter(isRecord).flatMap((model) => {
|
|
161
|
+
const architecture = isRecord(model.architecture) ? model.architecture : {};
|
|
162
|
+
const parameters = stringList(model.supported_parameters);
|
|
163
|
+
if (typeof model.id !== "string" || !parameters.includes("tools")) return [];
|
|
164
|
+
if (!stringList(architecture.output_modalities).includes("text")) return [];
|
|
165
|
+
const reasoning = isRecord(model.reasoning) ? model.reasoning : {};
|
|
166
|
+
const topProvider = isRecord(model.top_provider) ? model.top_provider : {};
|
|
167
|
+
return [{
|
|
168
|
+
id: `openrouter/${model.id}`,
|
|
169
|
+
provider: "openrouter",
|
|
170
|
+
native_model_id: model.id,
|
|
171
|
+
name: typeof model.name === "string" ? model.name : model.id,
|
|
172
|
+
context_window: positiveInteger(model.context_length),
|
|
173
|
+
max_output_tokens: positiveInteger(topProvider.max_completion_tokens),
|
|
174
|
+
input_modalities: stringList(architecture.input_modalities),
|
|
175
|
+
thinking_levels: gatewayThinkingLevels({
|
|
176
|
+
reasons: parameters.includes("reasoning"),
|
|
177
|
+
efforts: Array.isArray(reasoning.supported_efforts) ? stringList(reasoning.supported_efforts) : null,
|
|
178
|
+
canDisable: reasoning.mandatory !== true,
|
|
179
|
+
}),
|
|
180
|
+
price: isRecord(model.pricing) ? openRouterPrice(model.pricing) : null,
|
|
181
|
+
price_as_of: asOf,
|
|
182
|
+
}];
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function openRouterPrice(pricing: Record<string, unknown>): CatalogModelPrice | null {
|
|
187
|
+
const base = rates(pricing, "prompt", "completion");
|
|
188
|
+
if (base === null) return null;
|
|
189
|
+
// Overrides restate only the rates they change; time-of-day overrides are
|
|
190
|
+
// not context tiers.
|
|
191
|
+
const tiers = (Array.isArray(pricing.overrides) ? pricing.overrides : [])
|
|
192
|
+
.filter(isRecord)
|
|
193
|
+
.flatMap((override) => {
|
|
194
|
+
const threshold = positiveInteger(override.min_prompt_tokens);
|
|
195
|
+
if (threshold === null || "utc_days" in override || "utc_start" in override || "utc_end" in override) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const tier = rates({ ...pricing, ...override }, "prompt", "completion", override);
|
|
199
|
+
return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
|
|
200
|
+
});
|
|
201
|
+
return { ...base, tiers };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// https://vercel.com/docs/ai-gateway/models-and-providers
|
|
205
|
+
function vercelModels(data: unknown[], asOf: string): CatalogModel[] {
|
|
206
|
+
return data.filter(isRecord).flatMap((model) => {
|
|
207
|
+
const modalities = isRecord(model.modalities) ? model.modalities : {};
|
|
208
|
+
const parameters = stringList(model.supported_parameters);
|
|
209
|
+
if (typeof model.id !== "string" || model.type !== "language" || !parameters.includes("tools")) return [];
|
|
210
|
+
if (!stringList(modalities.output).includes("text")) return [];
|
|
211
|
+
const options = Array.isArray(model.reasoning_options) ? model.reasoning_options.filter(isRecord) : [];
|
|
212
|
+
const effort = options.find((option) => option.type === "effort");
|
|
213
|
+
return [{
|
|
214
|
+
id: `vercel-ai-gateway/${model.id}`,
|
|
215
|
+
provider: "vercel-ai-gateway",
|
|
216
|
+
native_model_id: model.id,
|
|
217
|
+
name: typeof model.name === "string" ? model.name : model.id,
|
|
218
|
+
context_window: positiveInteger(model.context_window),
|
|
219
|
+
max_output_tokens: positiveInteger(model.max_tokens),
|
|
220
|
+
input_modalities: stringList(modalities.input),
|
|
221
|
+
thinking_levels: gatewayThinkingLevels({
|
|
222
|
+
reasons: parameters.includes("reasoning") || options.length > 0,
|
|
223
|
+
efforts: effort === undefined ? null : stringList(effort.values),
|
|
224
|
+
canDisable: options.length === 0 || options.some((option) => option.type === "toggle"),
|
|
225
|
+
}),
|
|
226
|
+
price: isRecord(model.pricing) ? vercelPrice(model.pricing) : null,
|
|
227
|
+
price_as_of: asOf,
|
|
228
|
+
}];
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function vercelPrice(pricing: Record<string, unknown>): CatalogModelPrice | null {
|
|
233
|
+
const base = rates(pricing, "input", "output");
|
|
234
|
+
if (base === null) return null;
|
|
235
|
+
// Each rate lists its own {cost, min, max} ranges; a range covers prompts of
|
|
236
|
+
// at least `min` tokens, so it is the tier above `min - 1`.
|
|
237
|
+
const ranged = new Map<number, Record<string, unknown>>();
|
|
238
|
+
for (const key of ["input", "output", "input_cache_read", "input_cache_write"]) {
|
|
239
|
+
const ranges = Array.isArray(pricing[`${key}_tiers`]) ? pricing[`${key}_tiers`] as unknown[] : [];
|
|
240
|
+
for (const range of ranges.filter(isRecord)) {
|
|
241
|
+
const start = positiveInteger(range.min);
|
|
242
|
+
if (start === null || range.cost == null) continue;
|
|
243
|
+
ranged.set(start - 1, { ...ranged.get(start - 1), [key]: range.cost });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const tiers = [...ranged.entries()]
|
|
247
|
+
.sort(([left], [right]) => left - right)
|
|
248
|
+
.flatMap(([threshold, stated]) => {
|
|
249
|
+
const tier = rates({ ...pricing, ...stated }, "input", "output", stated);
|
|
250
|
+
return tier === null ? [] : [{ input_tokens_above: threshold, ...tier }];
|
|
251
|
+
});
|
|
252
|
+
return { ...base, tiers };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Base rates from per-token strings. A tier inherits input and output from
|
|
256
|
+
// the base price but states its own cache rates, or has none.
|
|
257
|
+
function rates(
|
|
258
|
+
pricing: Record<string, unknown>,
|
|
259
|
+
inputKey: string,
|
|
260
|
+
outputKey: string,
|
|
261
|
+
cacheRates: Record<string, unknown> = pricing,
|
|
262
|
+
): Omit<CatalogPriceTier, "input_tokens_above"> | null {
|
|
263
|
+
const input = perMillion(pricing[inputKey]);
|
|
264
|
+
const output = perMillion(pricing[outputKey]);
|
|
265
|
+
// Meta-routers with variable prices list negative rates.
|
|
266
|
+
if (input === null || output === null) return null;
|
|
267
|
+
return {
|
|
268
|
+
input,
|
|
269
|
+
output,
|
|
270
|
+
cached_input: perMillion(cacheRates.input_cache_read),
|
|
271
|
+
cache_write: perMillion(cacheRates.input_cache_write),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// USD per token as a decimal string -> USD per million tokens. Shifting the
|
|
276
|
+
// exponent in the string keeps "0.000001" exactly 1.
|
|
277
|
+
function perMillion(value: unknown): number | null {
|
|
278
|
+
const rate = typeof value === "string" ? Number(`${value.trim()}e6`)
|
|
279
|
+
: typeof value === "number" ? value * 1_000_000
|
|
280
|
+
: Number.NaN;
|
|
281
|
+
return Number.isFinite(rate) && rate >= 0 ? rate : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Gateways forward `reasoning.effort`; models that reason without a stated
|
|
285
|
+
// effort list take the standard low/medium/high levels.
|
|
286
|
+
function gatewayThinkingLevels(args: {
|
|
287
|
+
reasons: boolean;
|
|
288
|
+
efforts: string[] | null;
|
|
289
|
+
canDisable: boolean;
|
|
290
|
+
}): ReasoningEffort[] {
|
|
291
|
+
if (!args.reasons) return ["off"];
|
|
292
|
+
const efforts = args.efforts ?? ["low", "medium", "high"];
|
|
293
|
+
return orderedEfforts([
|
|
294
|
+
...efforts.map((effort) => effort === "none" ? "off" : effort),
|
|
295
|
+
...(args.canDisable ? ["off"] : []),
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function orderedEfforts(levels: readonly string[]): ReasoningEffort[] {
|
|
300
|
+
const accepted = new Set(levels.filter(isReasoningEffort));
|
|
301
|
+
return REASONING_EFFORTS.filter((effort) => accepted.has(effort));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function stringList(value: unknown): string[] {
|
|
305
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function positiveInteger(value: unknown): number | null {
|
|
309
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
310
|
+
}
|
|
@@ -11,12 +11,15 @@ export function executionProviderForModel(
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* The key that serves a model on a resolved router
|
|
14
|
+
* The key that serves a model on a resolved router: a platform-supplied
|
|
15
|
+
* credential first, then the resolved provider key. Missing declarations are
|
|
15
16
|
* user-owned so unknown platform data cannot trigger managed-key billing.
|
|
16
17
|
*/
|
|
17
18
|
export function keySourceForModel(
|
|
18
19
|
credentials: ResolvedRouter,
|
|
19
20
|
model: string,
|
|
20
21
|
): ProviderKeySource {
|
|
22
|
+
const supplied = credentials.provider_credentials?.[model];
|
|
23
|
+
if (supplied) return supplied.subscription ? "subscription" : "user";
|
|
21
24
|
return credentials.provider_key_sources?.[executionProviderForModel(credentials, model)] ?? "user";
|
|
22
25
|
}
|
|
@@ -33,6 +33,9 @@ const PROVIDER_BASE_URLS: Record<string, string> = {
|
|
|
33
33
|
anthropic: "https://api.anthropic.com",
|
|
34
34
|
fireworks: "https://api.fireworks.ai/inference/v1",
|
|
35
35
|
xai: "https://api.x.ai/v1",
|
|
36
|
+
// Meta's Model API serves OpenAI Responses and Chat Completions here, and
|
|
37
|
+
// Anthropic Messages at https://api.meta.ai.
|
|
38
|
+
meta: "https://api.meta.ai/v1",
|
|
36
39
|
// Pi-ai derives the effective AWS region from the endpoint unless the
|
|
37
40
|
// model config overrides base_url or the native id is an inference profile
|
|
38
41
|
// ARN, so this default only pins us-east-1 when nothing else is configured.
|
|
@@ -169,6 +169,28 @@ const MODEL_OFF_WIRE_NONE: ReadonlySet<string> = new Set([
|
|
|
169
169
|
"deepseek-ai/DeepSeek-V4.1-Flash-1017",
|
|
170
170
|
]);
|
|
171
171
|
|
|
172
|
+
// Wire values a provider's API accepts for thinking levels the Pi registry
|
|
173
|
+
// leaves unmapped. Meta's Model API takes reasoning effort "none" on every
|
|
174
|
+
// Muse model (dev.meta.ai/docs/coding-agents).
|
|
175
|
+
const PROVIDER_THINKING_LEVEL_OVERRIDES: Record<string, Record<string, string>> = {
|
|
176
|
+
meta: { off: "none" },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export function providerThinkingLevelOverrides(provider: string): Record<string, string> | undefined {
|
|
180
|
+
return PROVIDER_THINKING_LEVEL_OVERRIDES[provider];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Registered Pi metadata with the serving provider's thinking level overrides. */
|
|
184
|
+
export function withThinkingLevelOverrides(
|
|
185
|
+
provider: string,
|
|
186
|
+
registered: Record<string, unknown> | undefined,
|
|
187
|
+
): Record<string, unknown> | undefined {
|
|
188
|
+
const override = providerThinkingLevelOverrides(provider);
|
|
189
|
+
if (registered === undefined || override === undefined) return registered;
|
|
190
|
+
const map = isRecord(registered.thinkingLevelMap) ? registered.thinkingLevelMap : {};
|
|
191
|
+
return { ...registered, thinkingLevelMap: { ...map, ...override } };
|
|
192
|
+
}
|
|
193
|
+
|
|
172
194
|
export function offRequiresWireNone(modelId: string): boolean {
|
|
173
195
|
return MODEL_OFF_WIRE_NONE.has(canonicalModelId(modelId));
|
|
174
196
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nativeModelId,
|
|
3
|
+
providerForModel,
|
|
4
|
+
type RouterModelPrice,
|
|
5
|
+
} from "@mupt-ai/dari-router/policy-engine";
|
|
6
|
+
|
|
7
|
+
import type { ClientHarness, ResolvedRouterModelConfig, SubscriptionKind } from "../types.js";
|
|
8
|
+
|
|
9
|
+
const CODEX_BACKEND_URL = "https://chatgpt.com/backend-api";
|
|
10
|
+
|
|
11
|
+
// The model provider whose models each kind of plan serves.
|
|
12
|
+
const SUBSCRIPTION_MODEL_PROVIDERS: Record<SubscriptionKind, string> = {
|
|
13
|
+
openai_codex: "openai",
|
|
14
|
+
anthropic_claude_code: "anthropic",
|
|
15
|
+
meta_muse: "meta",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// The client each plan is limited to, as its provider limits it: a Claude Code
|
|
19
|
+
// plan serves only Claude Code, and a Muse Code plan only Muse Code.
|
|
20
|
+
const SUBSCRIPTION_CLIENTS: Partial<Record<SubscriptionKind, ClientHarness>> = {
|
|
21
|
+
anthropic_claude_code: "claude_code",
|
|
22
|
+
meta_muse: "muse_code",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a plan may serve a model for this client: each plan serves only its
|
|
27
|
+
* own provider's models, and only to the client its provider allows.
|
|
28
|
+
*/
|
|
29
|
+
export function subscriptionServes(
|
|
30
|
+
kind: SubscriptionKind,
|
|
31
|
+
model: string,
|
|
32
|
+
clientHarness: ClientHarness | undefined,
|
|
33
|
+
): boolean {
|
|
34
|
+
if (SUBSCRIPTION_MODEL_PROVIDERS[kind] !== providerForModel(model)) return false;
|
|
35
|
+
const client = SUBSCRIPTION_CLIENTS[kind];
|
|
36
|
+
return client === undefined || clientHarness === client;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A ChatGPT plan serves OpenAI models as the `openai-codex` provider through
|
|
40
|
+
// the Codex backend.
|
|
41
|
+
export function codexModelConfig(
|
|
42
|
+
modelId: string,
|
|
43
|
+
price: RouterModelPrice | null,
|
|
44
|
+
): ResolvedRouterModelConfig {
|
|
45
|
+
return {
|
|
46
|
+
provider: "openai-codex",
|
|
47
|
+
native_model_id: nativeModelId(modelId),
|
|
48
|
+
api: "openai-codex-responses",
|
|
49
|
+
base_url: CODEX_BACKEND_URL,
|
|
50
|
+
// A plan bills no tokens; the catalog price only feeds cost estimates.
|
|
51
|
+
cost: price ?? { input: 0, output: 0 },
|
|
52
|
+
fast_mode: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/platform/platform.ts
CHANGED
|
@@ -33,7 +33,10 @@ export type GatewayPlatform = {
|
|
|
33
33
|
* Optional. Supply the credential for one enabled model of this request,
|
|
34
34
|
* consulted after `resolveRouter` and before its provider keys (for the
|
|
35
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.
|
|
36
|
+
* per-tenant keys from its own store. Undefined keeps the resolved key. A
|
|
37
|
+
* credential with `subscription` serves the model on that plan, even on a
|
|
38
|
+
* strict router; a plan that cannot serve the model for this client (see
|
|
39
|
+
* SubscriptionKind) is ignored.
|
|
37
40
|
*/
|
|
38
41
|
resolveProviderCredential?(
|
|
39
42
|
request: ProviderCredentialRequest,
|
|
@@ -74,6 +77,7 @@ export type ProviderCredentialRequest = {
|
|
|
74
77
|
organizationId: string;
|
|
75
78
|
// The gateway key the request authenticated with.
|
|
76
79
|
apiKey: string;
|
|
80
|
+
clientHarness?: ClientHarness;
|
|
77
81
|
};
|
|
78
82
|
|
|
79
83
|
export type PrefixStateWrite = {
|
|
@@ -80,7 +80,9 @@ export function internalChatCompletionsToInteraction(request: ChatCompletionRequ
|
|
|
80
80
|
const tools = chatTools(request.tools);
|
|
81
81
|
toolsByWireName(tools);
|
|
82
82
|
const responseFormat = toInteractionResponseFormat(request.response_format);
|
|
83
|
-
|
|
83
|
+
// OpenAI spells the non-reasoning effort "none"; Dari spells it "off".
|
|
84
|
+
const requestedEffort: string | undefined = request.reasoning_effort;
|
|
85
|
+
const reasoningEffort = requestedEffort === "none" ? "off" : request.reasoning_effort;
|
|
84
86
|
return {
|
|
85
87
|
sourceProtocol: request.source_protocol ?? "openai_chat",
|
|
86
88
|
model: request.model,
|
|
@@ -214,7 +214,8 @@ function validateReasoning(request: ChatCompletionRequest): void {
|
|
|
214
214
|
"reasoning"
|
|
215
215
|
);
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
const effort: unknown = request.reasoning_effort;
|
|
218
|
+
if (effort !== undefined && effort !== "none" && !isReasoningEffort(effort)) {
|
|
218
219
|
throw new HttpError(
|
|
219
220
|
400,
|
|
220
221
|
"reasoning_effort is not supported.",
|
package/src/providers/pi.ts
CHANGED
|
@@ -29,7 +29,13 @@ import {
|
|
|
29
29
|
import { HttpError } from "../http/errors.js";
|
|
30
30
|
import { effectiveContextWindow } from "../models/context_limits.js";
|
|
31
31
|
import { defaultBaseUrl } from "../models/provider_defaults.js";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
contextWindowOverride,
|
|
34
|
+
inputModalitiesOverride,
|
|
35
|
+
providerThinkingLevelOverrides,
|
|
36
|
+
runtimeCompat,
|
|
37
|
+
withThinkingLevelOverrides,
|
|
38
|
+
} from "../models/runtime_overrides.js";
|
|
33
39
|
import { BEDROCK_API } from "./model_capabilities.js";
|
|
34
40
|
import { buildContext } from "./pi_context.js";
|
|
35
41
|
import { providerIdentityFromModel } from "../interaction/provider_continuation_state.js";
|
|
@@ -335,16 +341,21 @@ export function buildModel(
|
|
|
335
341
|
// A null catalog value delegates to Pi's registry when it knows the model.
|
|
336
342
|
// Custom OpenAI-compatible models are not necessarily registered, so keep
|
|
337
343
|
// the public effort set permissive and let the provider validate the wire
|
|
338
|
-
// value instead of silently treating an unknown model as off-only.
|
|
344
|
+
// value instead of silently treating an unknown model as off-only. A
|
|
345
|
+
// built-in provider with its own thinking levels (Meta) serves ids newer
|
|
346
|
+
// than the registry the same way, with only its key configured.
|
|
347
|
+
const providerThinking = providerThinkingLevelOverrides(provider);
|
|
339
348
|
const fallbackThinkingLevels =
|
|
340
|
-
config !== undefined
|
|
349
|
+
(config !== undefined || providerThinking !== undefined) &&
|
|
350
|
+
configuredThinkingLevels == null &&
|
|
351
|
+
registered === undefined
|
|
341
352
|
? REASONING_EFFORTS
|
|
342
353
|
: undefined;
|
|
343
354
|
const effectiveThinkingLevels = Array.isArray(configuredThinkingLevels)
|
|
344
355
|
? configuredThinkingLevels
|
|
345
356
|
: fallbackThinkingLevels;
|
|
346
357
|
const configuredThinking = effectiveThinkingLevels
|
|
347
|
-
? thinkingMetadata(effectiveThinkingLevels, registered)
|
|
358
|
+
? thinkingMetadata(effectiveThinkingLevels, registered ?? { thinkingLevelMap: providerThinking })
|
|
348
359
|
: null;
|
|
349
360
|
const defaultThinkingLevel = config?.default_thinking_level ?? undefined;
|
|
350
361
|
const api = config?.api ?? registered?.api ?? defaultApi(provider);
|
|
@@ -415,7 +426,10 @@ function resolveModelMetadata(
|
|
|
415
426
|
nativeId: string
|
|
416
427
|
): Record<string, unknown> | undefined {
|
|
417
428
|
const registry = registryProvider(provider, modelId);
|
|
418
|
-
return
|
|
429
|
+
return withThinkingLevelOverrides(
|
|
430
|
+
provider,
|
|
431
|
+
pi.getModel?.(registry, nativeId) ?? pi.getModel?.(registry, modelId),
|
|
432
|
+
);
|
|
419
433
|
}
|
|
420
434
|
|
|
421
435
|
// Dari's "azure" provider has no Pi registry of its own. One Foundry resource
|
|
@@ -491,7 +505,8 @@ function executionPricing(
|
|
|
491
505
|
}
|
|
492
506
|
|
|
493
507
|
function defaultApi(provider: string): string {
|
|
494
|
-
|
|
508
|
+
// Meta recommends Responses: its Chat Completions drops reasoning between turns.
|
|
509
|
+
if (provider === "openai" || provider === "xai" || provider === "meta") return "openai-responses";
|
|
495
510
|
if (provider === "anthropic") return "anthropic-messages";
|
|
496
511
|
if (provider === "amazon-bedrock") return BEDROCK_API;
|
|
497
512
|
if (provider === "azure") return "azure-openai-responses";
|
|
@@ -51,8 +51,10 @@ export function buildOptions(
|
|
|
51
51
|
const providerAuth: ResolvedProviderAuth = typeof auth === "string" ? { type: "bearer", apiKey: auth } : auth;
|
|
52
52
|
const { generation, toolChoice, parallelToolCalls, responseFormat } = request;
|
|
53
53
|
const headers: Record<string, string> = {};
|
|
54
|
+
const bearerHeader = sendsBearerToMessages(providerAuth, model);
|
|
55
|
+
if (bearerHeader) headers.authorization = `Bearer ${providerAuth.apiKey}`;
|
|
54
56
|
const options: Record<string, unknown> = {
|
|
55
|
-
...authenticationOptions(providerAuth, api),
|
|
57
|
+
...(bearerHeader ? {} : authenticationOptions(providerAuth, api)),
|
|
56
58
|
maxRetries: 0,
|
|
57
59
|
timeoutMs,
|
|
58
60
|
signal,
|
|
@@ -67,6 +69,9 @@ export function buildOptions(
|
|
|
67
69
|
if (isClaudeCodeOAuth(providerAuth, api)) {
|
|
68
70
|
headers["user-agent"] = request.claudeCodeUserAgent ?? CLAUDE_CODE_USER_AGENT;
|
|
69
71
|
}
|
|
72
|
+
if (model.provider === "meta" && request.museCodeClientId !== undefined) {
|
|
73
|
+
headers["x-client-id"] = request.museCodeClientId;
|
|
74
|
+
}
|
|
70
75
|
// Pi's Anthropic transport honors compat.supportsTemperature; the OpenAI
|
|
71
76
|
// transports forward temperature regardless, so drop it here for models
|
|
72
77
|
// that reject it instead of failing the whole request.
|
|
@@ -207,6 +212,17 @@ function authenticationOptions(auth: ResolvedProviderAuth, api: unknown): Bedroc
|
|
|
207
212
|
};
|
|
208
213
|
}
|
|
209
214
|
|
|
215
|
+
// Anthropic Messages endpoints outside Anthropic that take the key as a bearer
|
|
216
|
+
// token (Claude Code's ANTHROPIC_AUTH_TOKEN) and reject x-api-key.
|
|
217
|
+
const BEARER_MESSAGES_PROVIDERS: ReadonlySet<unknown> = new Set(["meta"]);
|
|
218
|
+
|
|
219
|
+
function sendsBearerToMessages(
|
|
220
|
+
auth: ResolvedProviderAuth,
|
|
221
|
+
model: Model,
|
|
222
|
+
): auth is Extract<ResolvedProviderAuth, { type: "bearer" }> {
|
|
223
|
+
return auth.type === "bearer" && isAnthropic(model.api) && BEARER_MESSAGES_PROVIDERS.has(model.provider);
|
|
224
|
+
}
|
|
225
|
+
|
|
210
226
|
function isClaudeCodeOAuth(auth: ResolvedProviderAuth, api: unknown): boolean {
|
|
211
227
|
return auth.type === "bearer" && isAnthropic(api) && auth.apiKey.includes("sk-ant-oat");
|
|
212
228
|
}
|
|
@@ -115,8 +115,10 @@ export function prepareProviderExecution(args: {
|
|
|
115
115
|
}
|
|
116
116
|
const provider = executionProviderForModel(args.credentials, model);
|
|
117
117
|
const supplied = args.credentials.provider_credentials?.[model];
|
|
118
|
-
const providerKeySource =
|
|
119
|
-
|
|
118
|
+
const providerKeySource = keySourceForModel(args.credentials, model);
|
|
119
|
+
// A strict router serves a plan only when the platform supplied it for this
|
|
120
|
+
// request, never a pooled or personal one it resolved.
|
|
121
|
+
if (args.credentials.strict === true && providerKeySource === "subscription" && !supplied) {
|
|
120
122
|
throw new HttpError(
|
|
121
123
|
400,
|
|
122
124
|
`Strict routers do not serve '${model}' from a personal subscription.`,
|
|
@@ -148,7 +150,9 @@ export function prepareProviderExecution(args: {
|
|
|
148
150
|
: {}),
|
|
149
151
|
provider,
|
|
150
152
|
providerKeySource,
|
|
151
|
-
|
|
153
|
+
// Only resolved personal subscriptions are held at their usage limit;
|
|
154
|
+
// the platform manages the plans it supplies.
|
|
155
|
+
...(providerKeySource === "subscription" && !supplied && args.credentials.attributed_user_id
|
|
152
156
|
? {
|
|
153
157
|
subscriptionUserId: args.credentials.attributed_user_id,
|
|
154
158
|
subscriptionCredentialId: args.credentials.subscription_credential_ids?.[provider],
|
|
@@ -5,7 +5,8 @@ import { providerFailureFromCaught } from "../runtime/provider_stream_outcome.js
|
|
|
5
5
|
import type { ProviderFetch } from "./response_capture.js";
|
|
6
6
|
|
|
7
7
|
// Gateways bill per call and say what they billed: OpenRouter as `usage.cost`
|
|
8
|
-
// on the final chunk, Vercel AI
|
|
8
|
+
// on the final chunk (plus the provider's charge on a BYOK call), Vercel AI
|
|
9
|
+
// Gateway as `providerMetadata.gateway.cost`.
|
|
9
10
|
// Pi prices usage from model rates and drops both, so the amount is read off
|
|
10
11
|
// the raw response bytes and replaces the priced total.
|
|
11
12
|
|
|
@@ -84,7 +85,20 @@ export function reportedCost(body: unknown): number | undefined {
|
|
|
84
85
|
const gateway = isRecord(body.providerMetadata) && isRecord(body.providerMetadata.gateway)
|
|
85
86
|
? body.providerMetadata.gateway
|
|
86
87
|
: undefined;
|
|
87
|
-
return
|
|
88
|
+
return openRouterCost(body.usage) ?? usd(gateway?.cost);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// On a BYOK call OpenRouter bills the provider's charge to the org's own key:
|
|
92
|
+
// `usage.cost` is only its fee (often 0), and the charge is in
|
|
93
|
+
// `cost_details.upstream_inference_cost`.
|
|
94
|
+
function openRouterCost(usage: unknown): number | undefined {
|
|
95
|
+
if (!isRecord(usage)) return undefined;
|
|
96
|
+
const cost = usd(usage.cost);
|
|
97
|
+
if (cost === undefined || usage.is_byok !== true) return cost;
|
|
98
|
+
const upstream = isRecord(usage.cost_details)
|
|
99
|
+
? usd(usage.cost_details.upstream_inference_cost)
|
|
100
|
+
: undefined;
|
|
101
|
+
return cost + (upstream ?? 0);
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
function usd(value: unknown): number | undefined {
|