@oh-my-pi/pi-ai 18.9.1 → 18.10.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/CHANGELOG.md +21 -0
- package/dist/types/auth-gateway/dispatch.d.ts +5 -3
- package/dist/types/judgment/index.d.ts +1 -1
- package/dist/types/judgment/native.d.ts +80 -0
- package/dist/types/judgment/openai-decisions.d.ts +3 -0
- package/dist/types/judgment/systemone.d.ts +7 -0
- package/dist/types/judgment/types.d.ts +2 -2
- package/dist/types/judgment/wire.d.ts +36 -0
- package/dist/types/providers/transform-messages.d.ts +14 -0
- package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
- package/dist/types/utils/retry-wait.d.ts +9 -0
- package/package.json +6 -6
- package/src/auth/sqlite-credential-store.ts +4 -4
- package/src/auth-gateway/dispatch.ts +7 -5
- package/src/auth-gateway/routes/systemone.ts +16 -12
- package/src/judgment/index.ts +1 -1
- package/src/judgment/native.ts +333 -0
- package/src/judgment/openai-decisions-predicate.md +9 -0
- package/src/judgment/openai-decisions.ts +129 -0
- package/src/judgment/systemone.ts +65 -0
- package/src/judgment/types.ts +2 -2
- package/src/judgment/wire.ts +42 -0
- package/src/providers/anthropic.ts +82 -70
- package/src/providers/cursor.ts +2 -6
- package/src/providers/devin.ts +20 -2
- package/src/providers/openai-responses.ts +7 -8
- package/src/providers/systemone-server.ts +1 -1
- package/src/providers/transform-messages.ts +146 -68
- package/src/registry/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/hooks/api-key.ts +1 -0
- package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
- package/src/usage/claude.ts +2 -6
- package/src/utils/empty-completion-retry.ts +2 -3
- package/src/utils/retry-wait.ts +19 -0
- package/dist/types/judgment/typesafe.d.ts +0 -49
- package/src/judgment/typesafe.ts +0 -183
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.9.2] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed `TypeSafeJudge` to `NativeJudge` and `TypeSafeApiError` to `JudgmentApiError`, including their associated option types. `JUDGMENT_ROUTES` has been removed; use `isJudgmentApi` or create a judge for catalog models with `createNativeJudge(model, { apiKey })`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added native judge support for OpenAI’s Decisions API and Cloudflare Workers AI decision models, including the `openai-decisions` and `workers-ai-systemone` APIs. The auth gateway’s System One route supports both providers.
|
|
14
|
+
- Added `createNativeJudge(model, { apiKey, headers, fetch })` for creating judges from catalog models with their configured request model IDs and question limits.
|
|
15
|
+
- Added Cloudflare Workers AI authentication with `loginCloudflareWorkersAi`, storing the API token and account ID; logging in again with the same token replaces the existing credential.
|
|
16
|
+
- Added automatic question batching to `NativeJudge` for endpoints with question limits, with up to four concurrent batches and merged answers and usage reporting. Usage from successful batches is reported through partial-usage callbacks and both session and auth-gateway usage ledgers, even if another batch fails.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Improved Anthropic prompt caching for long conversations, reducing memory usage while selecting cache breakpoints.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed Devin-routed OpenAI models returning `invalid_argument` on every turn after an existing session was switched from `openai-codex`.
|
|
25
|
+
|
|
5
26
|
## [18.9.1] - 2026-10-11
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -65,9 +65,11 @@ export declare function resolveGatewayApiKey(storage: AuthStorage, model: Model<
|
|
|
65
65
|
* to a sibling (usage-limit block vs credential invalidation by error class).
|
|
66
66
|
*
|
|
67
67
|
* `lastKey` tracks the most recent bearer so the switch step invalidates the
|
|
68
|
-
* credential that actually failed
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* credential that actually failed; a retry context's `previousKey` wins, since
|
|
69
|
+
* concurrent operations sharing one resolver (split judgment chunks) each know
|
|
70
|
+
* the bearer they sent. `onResolvedKey` observes every rotation; routes that
|
|
71
|
+
* retain provider session state use it to re-key the account lease, one-shot
|
|
72
|
+
* routes pass `undefined`.
|
|
71
73
|
*/
|
|
72
74
|
export declare function buildGatewayApiKeyResolver(storage: AuthStorage, model: Model<Api>, sessionId: string, initialKey: ResolvedApiKey, requestSignal: AbortSignal, format: string, peer: string, onResolvedKey?: (apiKey: string) => void): ApiKeyResolver;
|
|
73
75
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Api, FetchImpl } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import { type ApiKey } from "../auth-retry.js";
|
|
3
|
+
import * as AIError from "../error/index.js";
|
|
4
|
+
import type { Model, Usage } from "../types.js";
|
|
5
|
+
import { type Judge, type JudgeOptions, type JudgmentRequest, type JudgmentResult, type Questions } from "./types.js";
|
|
6
|
+
import type { JudgmentWire } from "./wire.js";
|
|
7
|
+
export declare const TYPESAFE_PROVIDER = "typesafe";
|
|
8
|
+
export declare const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
9
|
+
declare const JUDGMENT_WIRES: {
|
|
10
|
+
readonly typesafe: JudgmentWire;
|
|
11
|
+
readonly "openrouter-decisions": JudgmentWire;
|
|
12
|
+
readonly "workers-ai-systemone": JudgmentWire;
|
|
13
|
+
readonly "openai-decisions": JudgmentWire;
|
|
14
|
+
};
|
|
15
|
+
/** APIs {@link NativeJudge} can serve. */
|
|
16
|
+
export type JudgmentApi = keyof typeof JUDGMENT_WIRES;
|
|
17
|
+
/** Whether a catalog API answers judgments natively. */
|
|
18
|
+
export declare function isJudgmentApi(api: Api): api is JudgmentApi;
|
|
19
|
+
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
20
|
+
export declare function typesafeBaseUrl(): string;
|
|
21
|
+
/** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
|
|
22
|
+
export declare function typesafeModel(): string;
|
|
23
|
+
/** Credential and transport overrides for a judge; see {@link createNativeJudge}. */
|
|
24
|
+
export interface NativeJudgeConnection {
|
|
25
|
+
apiKey: ApiKey;
|
|
26
|
+
/** Static headers attached to judgment requests (e.g. proxy routing, gateway auth). */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
fetch?: FetchImpl;
|
|
29
|
+
}
|
|
30
|
+
export interface NativeJudgeOptions extends NativeJudgeConnection {
|
|
31
|
+
/** Wire dialect; defaults to TypeSafe's own API. */
|
|
32
|
+
api?: JudgmentApi;
|
|
33
|
+
/** Catalog provider reported on results; defaults to {@link TYPESAFE_PROVIDER}. */
|
|
34
|
+
provider?: string;
|
|
35
|
+
/** Defaults to {@link typesafeBaseUrl}. */
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
/** Wire model id; defaults to {@link typesafeModel}. */
|
|
38
|
+
model?: string;
|
|
39
|
+
/** Per-attempt timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Most questions per request the endpoint accepts, a positive integer; larger
|
|
43
|
+
* requests are split. Unset: no cap.
|
|
44
|
+
*/
|
|
45
|
+
maxQuestions?: number;
|
|
46
|
+
}
|
|
47
|
+
/** Per-call options for {@link NativeJudge.judge}. */
|
|
48
|
+
export interface NativeJudgeCallOptions extends JudgeOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Called before a split judgment rejects, with the summed usage of the chunks that had already
|
|
51
|
+
* succeeded: billed upstream, but their answers are discarded with the failure. Not called when
|
|
52
|
+
* no chunk succeeded or the request was not split.
|
|
53
|
+
*/
|
|
54
|
+
onPartialUsage?: (usage: Usage) => void;
|
|
55
|
+
}
|
|
56
|
+
/** Non-2xx response from a native judgment API. */
|
|
57
|
+
export declare class JudgmentApiError extends AIError.ProviderHttpError {
|
|
58
|
+
readonly name = "JudgmentApiError";
|
|
59
|
+
}
|
|
60
|
+
export declare class NativeJudge implements Judge {
|
|
61
|
+
#private;
|
|
62
|
+
readonly label: string;
|
|
63
|
+
readonly api: JudgmentApi;
|
|
64
|
+
readonly provider: string;
|
|
65
|
+
/** Wire model id sent with every request. */
|
|
66
|
+
readonly model: string;
|
|
67
|
+
readonly baseUrl: string;
|
|
68
|
+
constructor(options: NativeJudgeOptions);
|
|
69
|
+
judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: NativeJudgeCallOptions): Promise<JudgmentResult<Q>>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The {@link NativeJudge} for a catalog judge model: its API's dialect, wire id
|
|
73
|
+
* (`requestModelId ?? id`), base URL, and catalog question cap
|
|
74
|
+
* (`judgment-questions-max`). Shared by the coding agent's judge chain and the
|
|
75
|
+
* auth-gateway System One route so both address a model the same way.
|
|
76
|
+
*
|
|
77
|
+
* @throws ConfigurationError when the model's API is not a judgment API.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createNativeJudge(model: Model, connection: NativeJudgeConnection): NativeJudge;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type JudgmentWire } from "./wire.js";
|
|
2
|
+
/** TypeSafe's own route, shared by every System One server. */
|
|
3
|
+
export declare const typesafeWire: JudgmentWire;
|
|
4
|
+
/** OpenRouter Decisions: the System One body under `/api/alpha`. */
|
|
5
|
+
export declare const openrouterDecisionsWire: JudgmentWire;
|
|
6
|
+
/** Cloudflare Workers AI decision models (Clef) through the account-scoped `ai/run` route. */
|
|
7
|
+
export declare const workersAiWire: JudgmentWire;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* — about one {@link JudgmentState}. Every question in a request sees the same
|
|
8
8
|
* state and is answered independently, so callers batch independent questions
|
|
9
9
|
* into one call. The shape mirrors TypeSafe's System One API so the native
|
|
10
|
-
* backend ({@link
|
|
10
|
+
* backend ({@link NativeJudge}) forwards requests verbatim, while the text
|
|
11
11
|
* bridge ({@link TextJudge}) renders the same questions into keyword prompts
|
|
12
12
|
* for an ordinary chat model.
|
|
13
13
|
*/
|
|
@@ -75,7 +75,7 @@ export interface JudgmentRequest<Q extends Questions = Questions> {
|
|
|
75
75
|
questions: Q;
|
|
76
76
|
}
|
|
77
77
|
export interface JudgmentResult<Q extends Questions = Questions> {
|
|
78
|
-
/** Transport that produced the answers (`typesafe`, or the chat model's api). */
|
|
78
|
+
/** Transport that produced the answers (a judgment api such as `typesafe`, or the chat model's api). */
|
|
79
79
|
api: string;
|
|
80
80
|
provider: string;
|
|
81
81
|
model: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire dialects of the native judgment APIs: how {@link NativeJudge} addresses,
|
|
3
|
+
* encodes, and decodes one request for each API it serves. Internal to the
|
|
4
|
+
* judgment module; `native.ts` maps every judgment API to its dialect.
|
|
5
|
+
*/
|
|
6
|
+
import type { Usage } from "../types.js";
|
|
7
|
+
import type { JudgmentRequest } from "./types.js";
|
|
8
|
+
/** Where one attempt posts and which bearer token it sends. */
|
|
9
|
+
export interface JudgmentTarget {
|
|
10
|
+
url: string;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
/** One decoded response. */
|
|
14
|
+
export interface DecodedJudgment {
|
|
15
|
+
/** Model that answered, when the response names one. */
|
|
16
|
+
model: string | undefined;
|
|
17
|
+
/** Wire answers keyed by question id; {@link NativeJudge} checks each against its question's type. */
|
|
18
|
+
answers: Readonly<Record<string, unknown>>;
|
|
19
|
+
/** Question ids the model declined to answer. */
|
|
20
|
+
refused: readonly string[];
|
|
21
|
+
usage: Usage;
|
|
22
|
+
}
|
|
23
|
+
/** How one judgment API addresses, encodes, and decodes a request. */
|
|
24
|
+
export interface JudgmentWire {
|
|
25
|
+
/**
|
|
26
|
+
* Target of one attempt made with credential `key` against `model`.
|
|
27
|
+
* Throws when the credential cannot address the endpoint (e.g. no account id).
|
|
28
|
+
*/
|
|
29
|
+
target(baseUrl: string, model: string, key: string): JudgmentTarget;
|
|
30
|
+
/** Request body for `model`. */
|
|
31
|
+
encode(request: JudgmentRequest, model: string): unknown;
|
|
32
|
+
/** The response's answers and usage; `undefined` when the payload is not this API's response shape. */
|
|
33
|
+
decode(payload: unknown): DecodedJudgment | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** `value` when it is a finite number. */
|
|
36
|
+
export declare function finiteNumber(value: unknown): number | undefined;
|
|
@@ -9,6 +9,20 @@ import type { Api, AssistantMessage, Message, Model } from "../types.js";
|
|
|
9
9
|
* MUST not push a normalized id past this bound.
|
|
10
10
|
*/
|
|
11
11
|
export declare const MAX_TOOL_CALL_ID_LENGTH = 64;
|
|
12
|
+
/**
|
|
13
|
+
* OpenAI Responses-family APIs mint composite tool ids (`call_id|item_id`);
|
|
14
|
+
* opaque Chat Completions ids do not (openai-completions preserves same-model
|
|
15
|
+
* ids verbatim as provider correlation tokens), so ONLY these origins may be
|
|
16
|
+
* canonicalized to their `call_` component for pairing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isResponsesFamilyApi(api: Api | undefined): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The wire `call_id` component of a (possibly composite) Responses id: the
|
|
21
|
+
* FIRST segment before `|`. A degenerate `|itemId` (empty call half, pipe at
|
|
22
|
+
* index 0) keeps its full id so unrelated empty-half ids never collapse onto
|
|
23
|
+
* one empty-string bucket.
|
|
24
|
+
*/
|
|
25
|
+
export declare function responsesCallComponent(id: string): string;
|
|
12
26
|
export declare function appendDuplicateSuffix(originalId: string, suffix: string, maxLength: number): string;
|
|
13
27
|
/**
|
|
14
28
|
* Whether a tool-call name cannot belong to any declared tool: missing, empty,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Injected retry sleep: `StreamOptions.providerRetryWait` / `UsageFetchContext.retryWait`. */
|
|
2
|
+
export type RetryWaitHook = (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Waits `delayMs` between retry attempts. Honors the caller's retry-wait hook
|
|
5
|
+
* when one is set (tests and custom transports inject it to avoid real sleeps)
|
|
6
|
+
* and otherwise sleeps on `signal`, so an abort rejects instead of the retry
|
|
7
|
+
* proceeding.
|
|
8
|
+
*/
|
|
9
|
+
export declare function waitForRetry(retryWait: RetryWaitHook | undefined, delayMs: number, signal?: AbortSignal): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.10.0",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
158
|
+
"@oh-my-pi/omptype": "18.10.0",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.10.0",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.10.0",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.10.0",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.10.0"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
|
@@ -9,7 +9,7 @@ import * as fs from "node:fs/promises";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
11
11
|
import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
|
|
12
|
-
import {
|
|
12
|
+
import { parseCloudflareCredential } from "@oh-my-pi/pi-catalog/wire/cloudflare";
|
|
13
13
|
import {
|
|
14
14
|
getAgentDbPath,
|
|
15
15
|
getDbBusyTimeoutMs,
|
|
@@ -254,9 +254,9 @@ export function matchesReplacementCredential(
|
|
|
254
254
|
const incomingToken = parseAlibabaTokenPlanCredential(incoming.key)?.token;
|
|
255
255
|
return existingToken !== undefined && existingToken === incomingToken;
|
|
256
256
|
}
|
|
257
|
-
if (provider === "cloudflare-ai-gateway") {
|
|
258
|
-
const existingToken =
|
|
259
|
-
const incomingToken =
|
|
257
|
+
if (provider === "cloudflare-ai-gateway" || provider === "cloudflare-workers-ai") {
|
|
258
|
+
const existingToken = parseCloudflareCredential(existing.key)?.token;
|
|
259
|
+
const incomingToken = parseCloudflareCredential(incoming.key)?.token;
|
|
260
260
|
return existingToken !== undefined && existingToken === incomingToken;
|
|
261
261
|
}
|
|
262
262
|
return false;
|
|
@@ -195,9 +195,11 @@ function modelKeyOptions(model: Model<Api>, signal: AbortSignal): AuthApiKeyOpti
|
|
|
195
195
|
* to a sibling (usage-limit block vs credential invalidation by error class).
|
|
196
196
|
*
|
|
197
197
|
* `lastKey` tracks the most recent bearer so the switch step invalidates the
|
|
198
|
-
* credential that actually failed
|
|
199
|
-
*
|
|
200
|
-
*
|
|
198
|
+
* credential that actually failed; a retry context's `previousKey` wins, since
|
|
199
|
+
* concurrent operations sharing one resolver (split judgment chunks) each know
|
|
200
|
+
* the bearer they sent. `onResolvedKey` observes every rotation; routes that
|
|
201
|
+
* retain provider session state use it to re-key the account lease, one-shot
|
|
202
|
+
* routes pass `undefined`.
|
|
201
203
|
*/
|
|
202
204
|
export function buildGatewayApiKeyResolver(
|
|
203
205
|
storage: AuthStorage,
|
|
@@ -210,7 +212,7 @@ export function buildGatewayApiKeyResolver(
|
|
|
210
212
|
onResolvedKey?: (apiKey: string) => void,
|
|
211
213
|
): ApiKeyResolver {
|
|
212
214
|
let lastKey = initialKey.apiKey;
|
|
213
|
-
return async ({ lastChance, error, signal }) => {
|
|
215
|
+
return async ({ lastChance, error, signal, previousKey }) => {
|
|
214
216
|
const sig = signal ?? requestSignal;
|
|
215
217
|
if (error === undefined) {
|
|
216
218
|
lastKey = initialKey.apiKey;
|
|
@@ -231,7 +233,7 @@ export function buildGatewayApiKeyResolver(
|
|
|
231
233
|
model,
|
|
232
234
|
sessionId,
|
|
233
235
|
model.provider,
|
|
234
|
-
lastKey,
|
|
236
|
+
previousKey ?? lastKey,
|
|
235
237
|
error,
|
|
236
238
|
sig,
|
|
237
239
|
format,
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Same bearer auth, model resolution, broker credential lookup, and a/b/c
|
|
6
6
|
* credential rotation as the chat routes, dispatched through
|
|
7
|
-
* {@link
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* {@link NativeJudge} — pi-ai's client for every judgment API, which
|
|
8
|
+
* translates to the resolved model's own wire (OpenAI Decisions, Workers AI)
|
|
9
|
+
* — so 401 rotation and 429/529 backoff match omp's own judgments. A judgment
|
|
10
|
+
* is one JSON round-trip: no stream, no provider session state. Most judgment
|
|
11
|
+
* APIs report tokens only, so the cost is priced from the catalog model for the
|
|
12
|
+
* response header and the broker's observed-usage ledger.
|
|
12
13
|
*/
|
|
13
14
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
14
15
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
15
16
|
import { classifyGatewayError } from "../../error/gateway";
|
|
16
|
-
import {
|
|
17
|
+
import { createNativeJudge, isJudgmentApi } from "../../judgment/native";
|
|
17
18
|
import * as systemOne from "../../providers/systemone-server";
|
|
18
19
|
import { deterministicUuid } from "../../utils/deterministic-id";
|
|
19
20
|
import {
|
|
@@ -75,7 +76,7 @@ export async function handleSystemOne(
|
|
|
75
76
|
if (controller.signal.aborted) return aborted();
|
|
76
77
|
if ("status" in apiKey) return systemOne.formatError(apiKey.status, apiKey.type, apiKey.message);
|
|
77
78
|
|
|
78
|
-
const judge =
|
|
79
|
+
const judge = createNativeJudge(model, {
|
|
79
80
|
apiKey: buildGatewayApiKeyResolver(
|
|
80
81
|
bootOpts.storage,
|
|
81
82
|
model,
|
|
@@ -85,10 +86,6 @@ export async function handleSystemOne(
|
|
|
85
86
|
"systemone",
|
|
86
87
|
peer,
|
|
87
88
|
),
|
|
88
|
-
api: model.api,
|
|
89
|
-
provider: model.provider,
|
|
90
|
-
model: model.id,
|
|
91
|
-
baseUrl: model.baseUrl,
|
|
92
89
|
fetch: bootOpts.fetch,
|
|
93
90
|
});
|
|
94
91
|
|
|
@@ -103,7 +100,14 @@ export async function handleSystemOne(
|
|
|
103
100
|
});
|
|
104
101
|
|
|
105
102
|
try {
|
|
106
|
-
const result = await judge.judge(parsed.request, {
|
|
103
|
+
const result = await judge.judge(parsed.request, {
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
// A split judgment that fails still billed the chunks that succeeded.
|
|
106
|
+
onPartialUsage: usage => {
|
|
107
|
+
if (usage.cost.total === 0) calculateCost(model, usage);
|
|
108
|
+
recordGatewayUsage(bootOpts.storage, model, client, usage);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
107
111
|
// Encode before pricing: the body's `cost` means "upstream billed this"
|
|
108
112
|
// (OpenRouter), while the header and ledger carry the catalog estimate
|
|
109
113
|
// when the upstream reported tokens only (TypeSafe).
|