@oh-my-pi/pi-ai 18.9.0 → 18.9.2
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 +31 -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/amazon-bedrock.ts +6 -5
- 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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
2
2
|
import { apiRouteFor } from "@oh-my-pi/pi-catalog/compat/behavior";
|
|
3
|
+
import { parseCloudflareCredential } from "@oh-my-pi/pi-catalog/wire/cloudflare";
|
|
3
4
|
import {
|
|
4
5
|
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
|
|
5
6
|
CLOUDFLARE_AI_GATEWAY_BASE_URL,
|
|
6
7
|
CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL,
|
|
7
8
|
CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL,
|
|
8
|
-
parseCloudflareAiGatewayCredential,
|
|
9
9
|
} from "@oh-my-pi/pi-catalog/wire/cloudflare-ai-gateway";
|
|
10
10
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import { NO_AUTH_SENTINEL } from "../auth-retry";
|
|
@@ -83,7 +83,7 @@ export const cloudflareAiGatewayTransport: ProviderTransport = {
|
|
|
83
83
|
return model;
|
|
84
84
|
},
|
|
85
85
|
prepareRequest: (model, options) => {
|
|
86
|
-
const credential =
|
|
86
|
+
const credential = parseCloudflareCredential(options.apiKey ?? $env.CLOUDFLARE_AI_GATEWAY_API_KEY ?? "");
|
|
87
87
|
if (!credential) return { model, options };
|
|
88
88
|
const accountId = credential.accountId ?? $env.CLOUDFLARE_ACCOUNT_ID;
|
|
89
89
|
const gatewayId = credential.gatewayId ?? $env.CLOUDFLARE_GATEWAY_ID;
|
|
@@ -14,6 +14,7 @@ export const API_KEY_LOGIN_HOOKS: Record<string, Lazy<LoginHook>> = {
|
|
|
14
14
|
"alibaba-coding-plan": () => import("../oauth/alibaba-coding-plan").then(m => m.loginAlibabaCodingPlan),
|
|
15
15
|
"alibaba-token-plan": () => import("../oauth/alibaba-token-plan").then(m => m.loginAlibabaTokenPlan),
|
|
16
16
|
"cloudflare-ai-gateway": () => import("../oauth/cloudflare-ai-gateway").then(m => m.loginCloudflareAiGateway),
|
|
17
|
+
"cloudflare-workers-ai": () => import("../oauth/cloudflare-workers-ai").then(m => m.loginCloudflareWorkersAi),
|
|
17
18
|
kilo: () => import("../oauth/kilo").then(m => m.loginKilo),
|
|
18
19
|
xiaomi: () => import("../oauth/xiaomi").then(m => m.loginXiaomi),
|
|
19
20
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serializeCloudflareCredential } from "@oh-my-pi/pi-catalog/wire/cloudflare";
|
|
2
2
|
import * as AIError from "../../error";
|
|
3
3
|
import type { OAuthController } from "./types";
|
|
4
4
|
|
|
@@ -35,5 +35,5 @@ export async function loginCloudflareAiGateway(options: OAuthController): Promis
|
|
|
35
35
|
if (options.signal?.aborted) throw new AIError.LoginCancelledError();
|
|
36
36
|
if (!gatewayId.trim()) throw new AIError.ConfigurationError("Cloudflare AI Gateway ID is required");
|
|
37
37
|
|
|
38
|
-
return
|
|
38
|
+
return serializeCloudflareCredential({ token: apiKey, accountId, gatewayId });
|
|
39
39
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { serializeCloudflareCredential } from "@oh-my-pi/pi-catalog/wire/cloudflare";
|
|
2
|
+
import * as AIError from "../../error";
|
|
3
|
+
import type { OAuthController } from "./types";
|
|
4
|
+
|
|
5
|
+
const AUTH_URL = "https://dash.cloudflare.com/profile/api-tokens";
|
|
6
|
+
|
|
7
|
+
/** Collect the Workers AI API token and account id used by CLI, setup-wizard, and TUI login callers. */
|
|
8
|
+
export async function loginCloudflareWorkersAi(options: OAuthController): Promise<string> {
|
|
9
|
+
if (!options.onPrompt) {
|
|
10
|
+
throw new AIError.OnPromptRequiredError("Cloudflare Workers AI");
|
|
11
|
+
}
|
|
12
|
+
options.onAuth?.({
|
|
13
|
+
url: AUTH_URL,
|
|
14
|
+
instructions: "Create an API token with Workers AI Read and Edit permissions, then copy it here.",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const token = await options.onPrompt({
|
|
18
|
+
message: "Paste your Cloudflare API token",
|
|
19
|
+
placeholder: "Workers AI API token",
|
|
20
|
+
});
|
|
21
|
+
if (options.signal?.aborted) throw new AIError.LoginCancelledError();
|
|
22
|
+
if (!token.trim()) throw new AIError.ApiKeyRequiredError();
|
|
23
|
+
|
|
24
|
+
const accountId = await options.onPrompt({
|
|
25
|
+
message: "Enter your Cloudflare account ID",
|
|
26
|
+
placeholder: "32-character account ID",
|
|
27
|
+
});
|
|
28
|
+
if (options.signal?.aborted) throw new AIError.LoginCancelledError();
|
|
29
|
+
if (!accountId.trim()) throw new AIError.ConfigurationError("Cloudflare account ID is required");
|
|
30
|
+
|
|
31
|
+
return serializeCloudflareCredential({ token, accountId });
|
|
32
|
+
}
|
package/src/usage/claude.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { scheduler } from "node:timers/promises";
|
|
2
1
|
import { classifyModel } from "@oh-my-pi/pi-catalog/compat/taxonomy";
|
|
3
2
|
import { isAnthropicOAuthToken, toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
4
3
|
import * as AIError from "../error";
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
type UsageWindow,
|
|
17
16
|
} from "../usage";
|
|
18
17
|
import { isRecord } from "../utils";
|
|
18
|
+
import { waitForRetry } from "../utils/retry-wait";
|
|
19
19
|
import { buildClaudeOAuthHeaders, claudeOAuthBaseUrls } from "./claude-api";
|
|
20
20
|
import { listClaudeResetCredits, parseClaudeResetCreditsFromUsagePayload } from "./claude-reset";
|
|
21
21
|
import { HOUR_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared";
|
|
@@ -240,11 +240,7 @@ async function waitBeforeRetry(
|
|
|
240
240
|
if (attempt >= MAX_ATTEMPTS - 1) return false;
|
|
241
241
|
try {
|
|
242
242
|
const delayMs = retryDelayMs(attempt, retryAfter);
|
|
243
|
-
|
|
244
|
-
await retryWait(delayMs, signal);
|
|
245
|
-
} else {
|
|
246
|
-
await scheduler.wait(delayMs, { signal });
|
|
247
|
-
}
|
|
243
|
+
await waitForRetry(retryWait, delayMs, signal);
|
|
248
244
|
return !signal?.aborted;
|
|
249
245
|
} catch (error) {
|
|
250
246
|
if (isAbortError(error, signal)) return false;
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* valid provider result. Transient-error retries use the shared provider error
|
|
14
14
|
* classifier and are separately bounded by the caller's policy.
|
|
15
15
|
*/
|
|
16
|
-
import { scheduler } from "node:timers/promises";
|
|
17
16
|
import * as AIError from "../error";
|
|
18
17
|
import type { AssistantMessage, AssistantMessageEvent, Context } from "../types";
|
|
19
18
|
import { AssistantMessageEventStream } from "./event-stream";
|
|
19
|
+
import { waitForRetry } from "./retry-wait";
|
|
20
20
|
|
|
21
21
|
export const MAX_EMPTY_COMPLETION_RETRIES = 2;
|
|
22
22
|
export const EMPTY_COMPLETION_BASE_DELAY_MS = 500;
|
|
@@ -169,8 +169,7 @@ export function withReplaySafeStreamRetry<M, O extends StreamRetryOptions>(
|
|
|
169
169
|
|
|
170
170
|
if (delayMs !== undefined && !signal?.aborted) {
|
|
171
171
|
try {
|
|
172
|
-
|
|
173
|
-
else await scheduler.wait(delayMs, { signal });
|
|
172
|
+
await waitForRetry(options?.providerRetryWait, delayMs, signal);
|
|
174
173
|
} catch (waitError) {
|
|
175
174
|
flush();
|
|
176
175
|
if (signal?.aborted) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { scheduler } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
/** Injected retry sleep: `StreamOptions.providerRetryWait` / `UsageFetchContext.retryWait`. */
|
|
4
|
+
export type RetryWaitHook = (delayMs: number, signal?: AbortSignal) => Promise<void>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Waits `delayMs` between retry attempts. Honors the caller's retry-wait hook
|
|
8
|
+
* when one is set (tests and custom transports inject it to avoid real sleeps)
|
|
9
|
+
* and otherwise sleeps on `signal`, so an abort rejects instead of the retry
|
|
10
|
+
* proceeding.
|
|
11
|
+
*/
|
|
12
|
+
export async function waitForRetry(
|
|
13
|
+
retryWait: RetryWaitHook | undefined,
|
|
14
|
+
delayMs: number,
|
|
15
|
+
signal?: AbortSignal,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
if (retryWait) await retryWait(delayMs, signal);
|
|
18
|
+
else await scheduler.wait(delayMs, { signal });
|
|
19
|
+
}
|
|
@@ -1,49 +0,0 @@
|
|
|
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 Judge, type JudgeOptions, type JudgmentRequest, type JudgmentResult, type Questions } from "./types.js";
|
|
5
|
-
export declare const TYPESAFE_PROVIDER = "typesafe";
|
|
6
|
-
export declare const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
7
|
-
/** Judgment `POST` path under a model's base URL, per System One–compatible API. */
|
|
8
|
-
export declare const JUDGMENT_ROUTES: {
|
|
9
|
-
readonly typesafe: "/v1/systemone";
|
|
10
|
-
readonly "openrouter-decisions": "/decisions";
|
|
11
|
-
};
|
|
12
|
-
/** APIs {@link TypeSafeJudge} can serve. */
|
|
13
|
-
export type JudgmentApi = keyof typeof JUDGMENT_ROUTES;
|
|
14
|
-
/** Whether a catalog API answers System One judgments natively. */
|
|
15
|
-
export declare function isJudgmentApi(api: Api): api is JudgmentApi;
|
|
16
|
-
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
17
|
-
export declare function typesafeBaseUrl(): string;
|
|
18
|
-
/** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
|
|
19
|
-
export declare function typesafeModel(): string;
|
|
20
|
-
export interface TypeSafeJudgeOptions {
|
|
21
|
-
apiKey: ApiKey;
|
|
22
|
-
/** Wire route; defaults to TypeSafe's own API. */
|
|
23
|
-
api?: JudgmentApi;
|
|
24
|
-
/** Catalog provider reported on results; defaults to {@link TYPESAFE_PROVIDER}. */
|
|
25
|
-
provider?: string;
|
|
26
|
-
/** Defaults to {@link typesafeBaseUrl}. */
|
|
27
|
-
baseUrl?: string;
|
|
28
|
-
/** Defaults to {@link typesafeModel}. */
|
|
29
|
-
model?: string;
|
|
30
|
-
/** Static headers attached to judgment requests (e.g. proxy routing, gateway auth). */
|
|
31
|
-
headers?: Record<string, string>;
|
|
32
|
-
fetch?: FetchImpl;
|
|
33
|
-
/** Per-attempt timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
34
|
-
timeoutMs?: number;
|
|
35
|
-
}
|
|
36
|
-
/** Non-2xx response from the TypeSafe API. */
|
|
37
|
-
export declare class TypeSafeApiError extends AIError.ProviderHttpError {
|
|
38
|
-
readonly name = "TypeSafeApiError";
|
|
39
|
-
}
|
|
40
|
-
export declare class TypeSafeJudge implements Judge {
|
|
41
|
-
#private;
|
|
42
|
-
readonly label: string;
|
|
43
|
-
readonly api: JudgmentApi;
|
|
44
|
-
readonly provider: string;
|
|
45
|
-
readonly model: string;
|
|
46
|
-
readonly baseUrl: string;
|
|
47
|
-
constructor(options: TypeSafeJudgeOptions);
|
|
48
|
-
judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>>;
|
|
49
|
-
}
|
package/src/judgment/typesafe.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TypeSafe System One client: the native {@link Judge} backend.
|
|
3
|
-
*
|
|
4
|
-
* Forwards a {@link JudgmentRequest} verbatim to the judgment route of its
|
|
5
|
-
* API ({@link JUDGMENT_ROUTES}) and maps the typed answers back. TypeSafe's
|
|
6
|
-
* own `POST /v1/systemone` and OpenRouter's `POST /api/alpha/decisions` share
|
|
7
|
-
* the request and answer wire shape, so one client serves both. Credentials
|
|
8
|
-
* flow through {@link withAuth}, so a stored key rotates on 401/403 exactly
|
|
9
|
-
* like chat providers; transient 429/5xx responses retry with bounded,
|
|
10
|
-
* `retry-after`-aware backoff.
|
|
11
|
-
*
|
|
12
|
-
* Environment (mirrors the official SDK): `TYPESAFE_API_KEY` is resolved by
|
|
13
|
-
* the auth registry (`rules/auth/typesafe.kdl`), `TYPESAFE_BASE_URL`
|
|
14
|
-
* overrides the API root, `TYPESAFE_DEFAULT_MODEL` the model.
|
|
15
|
-
*/
|
|
16
|
-
import { TYPESAFE_DEFAULT_BASE_URL } from "@oh-my-pi/pi-catalog/discovery";
|
|
17
|
-
import type { Api, FetchImpl } from "@oh-my-pi/pi-catalog/types";
|
|
18
|
-
import { $env } from "@oh-my-pi/pi-utils";
|
|
19
|
-
import { type ApiKey, withAuth } from "../auth-retry";
|
|
20
|
-
import * as AIError from "../error";
|
|
21
|
-
import { getRetryAfterMsFromHeaders } from "../utils/retry-after";
|
|
22
|
-
import {
|
|
23
|
-
type Answer,
|
|
24
|
-
type Judge,
|
|
25
|
-
type JudgeOptions,
|
|
26
|
-
type JudgmentRequest,
|
|
27
|
-
type JudgmentResult,
|
|
28
|
-
type Questions,
|
|
29
|
-
tokenUsage,
|
|
30
|
-
} from "./types";
|
|
31
|
-
|
|
32
|
-
export const TYPESAFE_PROVIDER = "typesafe";
|
|
33
|
-
export const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
34
|
-
|
|
35
|
-
/** Judgment `POST` path under a model's base URL, per System One–compatible API. */
|
|
36
|
-
export const JUDGMENT_ROUTES = {
|
|
37
|
-
typesafe: "/v1/systemone",
|
|
38
|
-
"openrouter-decisions": "/decisions",
|
|
39
|
-
} as const satisfies Partial<Record<Api, string>>;
|
|
40
|
-
|
|
41
|
-
/** APIs {@link TypeSafeJudge} can serve. */
|
|
42
|
-
export type JudgmentApi = keyof typeof JUDGMENT_ROUTES;
|
|
43
|
-
|
|
44
|
-
/** Whether a catalog API answers System One judgments natively. */
|
|
45
|
-
export function isJudgmentApi(api: Api): api is JudgmentApi {
|
|
46
|
-
return Object.hasOwn(JUDGMENT_ROUTES, api);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
50
|
-
export function typesafeBaseUrl(): string {
|
|
51
|
-
return ($env.TYPESAFE_BASE_URL?.trim() || TYPESAFE_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** `TYPESAFE_DEFAULT_MODEL` when set, else {@link TYPESAFE_DEFAULT_MODEL}. */
|
|
55
|
-
export function typesafeModel(): string {
|
|
56
|
-
return $env.TYPESAFE_DEFAULT_MODEL?.trim() || TYPESAFE_DEFAULT_MODEL;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface TypeSafeJudgeOptions {
|
|
60
|
-
apiKey: ApiKey;
|
|
61
|
-
/** Wire route; defaults to TypeSafe's own API. */
|
|
62
|
-
api?: JudgmentApi;
|
|
63
|
-
/** Catalog provider reported on results; defaults to {@link TYPESAFE_PROVIDER}. */
|
|
64
|
-
provider?: string;
|
|
65
|
-
/** Defaults to {@link typesafeBaseUrl}. */
|
|
66
|
-
baseUrl?: string;
|
|
67
|
-
/** Defaults to {@link typesafeModel}. */
|
|
68
|
-
model?: string;
|
|
69
|
-
/** Static headers attached to judgment requests (e.g. proxy routing, gateway auth). */
|
|
70
|
-
headers?: Record<string, string>;
|
|
71
|
-
fetch?: FetchImpl;
|
|
72
|
-
/** Per-attempt timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
73
|
-
timeoutMs?: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Non-2xx response from the TypeSafe API. */
|
|
77
|
-
export class TypeSafeApiError extends AIError.ProviderHttpError {
|
|
78
|
-
override readonly name = "TypeSafeApiError";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
82
|
-
const MAX_ATTEMPTS = 3;
|
|
83
|
-
const BACKOFF_BASE_MS = 500;
|
|
84
|
-
const BACKOFF_MAX_MS = 5_000;
|
|
85
|
-
|
|
86
|
-
interface SystemOneResponse {
|
|
87
|
-
model: string;
|
|
88
|
-
answers: Record<string, Answer>;
|
|
89
|
-
/** OpenRouter adds the billed `cost` in USD; some routes omit token counts. */
|
|
90
|
-
usage: { input_tokens?: number; output_tokens?: number; cost?: number };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Server hint wins (capped); otherwise exponential backoff from {@link BACKOFF_BASE_MS}. */
|
|
94
|
-
function backoffMs(attempt: number, headers: Headers | undefined): number {
|
|
95
|
-
const hinted = headers === undefined ? undefined : getRetryAfterMsFromHeaders(headers);
|
|
96
|
-
if (hinted !== undefined) return Math.min(hinted, BACKOFF_MAX_MS);
|
|
97
|
-
return Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_MAX_MS);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export class TypeSafeJudge implements Judge {
|
|
101
|
-
readonly label: string;
|
|
102
|
-
readonly api: JudgmentApi;
|
|
103
|
-
readonly provider: string;
|
|
104
|
-
readonly model: string;
|
|
105
|
-
readonly baseUrl: string;
|
|
106
|
-
readonly #apiKey: ApiKey;
|
|
107
|
-
readonly #headers: Record<string, string> | undefined;
|
|
108
|
-
readonly #fetch: FetchImpl;
|
|
109
|
-
readonly #timeoutMs: number;
|
|
110
|
-
|
|
111
|
-
constructor(options: TypeSafeJudgeOptions) {
|
|
112
|
-
this.#apiKey = options.apiKey;
|
|
113
|
-
this.api = options.api ?? TYPESAFE_PROVIDER;
|
|
114
|
-
this.provider = options.provider ?? TYPESAFE_PROVIDER;
|
|
115
|
-
this.baseUrl = (options.baseUrl ?? typesafeBaseUrl()).replace(/\/+$/, "");
|
|
116
|
-
this.model = options.model ?? typesafeModel();
|
|
117
|
-
this.#headers = options.headers;
|
|
118
|
-
this.#fetch = options.fetch ?? fetch;
|
|
119
|
-
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
120
|
-
this.label = `${this.provider}/${this.model}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>> {
|
|
124
|
-
const body = JSON.stringify({ state: request.state, model: this.model, questions: request.questions });
|
|
125
|
-
const signal = options?.signal;
|
|
126
|
-
const response = await withAuth(
|
|
127
|
-
this.#apiKey,
|
|
128
|
-
key => this.#attempt<SystemOneResponse>(JUDGMENT_ROUTES[this.api], body, key, signal),
|
|
129
|
-
{ signal },
|
|
130
|
-
);
|
|
131
|
-
for (const id in request.questions) {
|
|
132
|
-
const answer = response.answers[id];
|
|
133
|
-
if (answer === undefined || answer.type !== request.questions[id].type) {
|
|
134
|
-
throw new AIError.ProviderResponseError(
|
|
135
|
-
`${this.label} response is missing a "${request.questions[id].type}" answer for question "${id}"`,
|
|
136
|
-
{ provider: this.provider, kind: "envelope" },
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
api: this.api,
|
|
142
|
-
provider: this.provider,
|
|
143
|
-
model: response.model,
|
|
144
|
-
answers: response.answers as JudgmentResult<Q>["answers"],
|
|
145
|
-
usage: tokenUsage(response.usage.input_tokens, response.usage.output_tokens, response.usage.cost),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async #attempt<T>(path: string, body: string, key: string, signal: AbortSignal | undefined): Promise<T> {
|
|
150
|
-
const url = `${this.baseUrl}${path}`;
|
|
151
|
-
const headers: Record<string, string> = {
|
|
152
|
-
...this.#headers,
|
|
153
|
-
Authorization: `Bearer ${key}`,
|
|
154
|
-
Accept: "application/json",
|
|
155
|
-
"Content-Type": "application/json",
|
|
156
|
-
};
|
|
157
|
-
for (let attempt = 0; ; attempt++) {
|
|
158
|
-
signal?.throwIfAborted();
|
|
159
|
-
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
160
|
-
let response: Response;
|
|
161
|
-
try {
|
|
162
|
-
response = await this.#fetch(url, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers,
|
|
165
|
-
body,
|
|
166
|
-
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
167
|
-
});
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (signal?.aborted || attempt + 1 >= MAX_ATTEMPTS) throw error;
|
|
170
|
-
await Bun.sleep(backoffMs(attempt, undefined));
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (response.ok) return (await response.json()) as T;
|
|
174
|
-
const text = await response.text();
|
|
175
|
-
const error = new TypeSafeApiError(`${this.label} API error (${response.status}): ${text}`, response.status, {
|
|
176
|
-
headers: response.headers,
|
|
177
|
-
});
|
|
178
|
-
const transient = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
179
|
-
if (!transient || attempt + 1 >= MAX_ATTEMPTS) throw error;
|
|
180
|
-
await Bun.sleep(backoffMs(attempt, response.headers));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|