@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/types/auth-gateway/dispatch.d.ts +5 -3
  3. package/dist/types/judgment/index.d.ts +1 -1
  4. package/dist/types/judgment/native.d.ts +80 -0
  5. package/dist/types/judgment/openai-decisions.d.ts +3 -0
  6. package/dist/types/judgment/systemone.d.ts +7 -0
  7. package/dist/types/judgment/types.d.ts +2 -2
  8. package/dist/types/judgment/wire.d.ts +36 -0
  9. package/dist/types/providers/transform-messages.d.ts +14 -0
  10. package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
  11. package/dist/types/utils/retry-wait.d.ts +9 -0
  12. package/package.json +6 -6
  13. package/src/auth/sqlite-credential-store.ts +4 -4
  14. package/src/auth-gateway/dispatch.ts +7 -5
  15. package/src/auth-gateway/routes/systemone.ts +16 -12
  16. package/src/judgment/index.ts +1 -1
  17. package/src/judgment/native.ts +333 -0
  18. package/src/judgment/openai-decisions-predicate.md +9 -0
  19. package/src/judgment/openai-decisions.ts +129 -0
  20. package/src/judgment/systemone.ts +65 -0
  21. package/src/judgment/types.ts +2 -2
  22. package/src/judgment/wire.ts +42 -0
  23. package/src/providers/anthropic.ts +82 -70
  24. package/src/providers/cursor.ts +2 -6
  25. package/src/providers/devin.ts +20 -2
  26. package/src/providers/openai-responses.ts +7 -8
  27. package/src/providers/systemone-server.ts +1 -1
  28. package/src/providers/transform-messages.ts +146 -68
  29. package/src/registry/cloudflare-ai-gateway.ts +2 -2
  30. package/src/registry/hooks/api-key.ts +1 -0
  31. package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
  32. package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
  33. package/src/usage/claude.ts +2 -6
  34. package/src/utils/empty-completion-retry.ts +2 -3
  35. package/src/utils/retry-wait.ts +19 -0
  36. package/dist/types/judgment/typesafe.d.ts +0 -49
  37. 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. `onResolvedKey` observes every rotation;
69
- * routes that retain provider session state use it to re-key the account
70
- * lease, one-shot routes pass `undefined`.
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
  /**
@@ -1,5 +1,5 @@
1
1
  export * from "./chat.js";
2
+ export * from "./native.js";
2
3
  export * from "./question-file.js";
3
4
  export * from "./text.js";
4
5
  export * from "./types.js";
5
- export * from "./typesafe.js";
@@ -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,3 @@
1
+ import { type JudgmentWire } from "./wire.js";
2
+ /** OpenAI's `/v1/decisions` route. */
3
+ export declare const openaiDecisionsWire: JudgmentWire;
@@ -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 TypeSafeJudge}) forwards requests verbatim, while the text
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,3 @@
1
+ import type { OAuthController } from "./types.js";
2
+ /** Collect the Workers AI API token and account id used by CLI, setup-wizard, and TUI login callers. */
3
+ export declare function loginCloudflareWorkersAi(options: OAuthController): Promise<string>;
@@ -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.9.1",
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.9.1",
159
- "@oh-my-pi/pi-catalog": "18.9.1",
160
- "@oh-my-pi/pi-natives": "18.9.1",
161
- "@oh-my-pi/pi-utils": "18.9.1",
162
- "@oh-my-pi/pi-wire": "18.9.1"
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 { parseCloudflareAiGatewayCredential } from "@oh-my-pi/pi-catalog/wire/cloudflare-ai-gateway";
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 = parseCloudflareAiGatewayCredential(existing.key)?.token;
259
- const incomingToken = parseCloudflareAiGatewayCredential(incoming.key)?.token;
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. `onResolvedKey` observes every rotation;
199
- * routes that retain provider session state use it to re-key the account
200
- * lease, one-shot routes pass `undefined`.
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 TypeSafeJudge} — pi-ai's client for this wire — so 401 rotation and
8
- * 429/529 backoff match omp's own judgments. A judgment is one JSON
9
- * round-trip: no stream, no provider session state. TypeSafe reports tokens
10
- * only, so the cost is priced from the catalog model for the response header
11
- * and the broker's observed-usage ledger.
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 { isJudgmentApi, TypeSafeJudge } from "../../judgment/typesafe";
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 = new TypeSafeJudge({
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, { signal: controller.signal });
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).
@@ -1,5 +1,5 @@
1
1
  export * from "./chat";
2
+ export * from "./native";
2
3
  export * from "./question-file";
3
4
  export * from "./text";
4
5
  export * from "./types";
5
- export * from "./typesafe";