@f5-sales-demo/pi-ai 19.51.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,176 @@
1
+ import { $env } from "@f5xc-salesdemos/pi-utils";
2
+
3
+ const DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS = 120_000;
4
+ const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 60_000;
5
+
6
+ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
7
+ if (value === undefined) return fallback;
8
+ const parsed = Number(value);
9
+ if (!Number.isFinite(parsed)) return fallback;
10
+ if (parsed <= 0) return undefined;
11
+ return Math.trunc(parsed);
12
+ }
13
+
14
+ /**
15
+ * Returns the idle timeout used for OpenAI-family streaming transports.
16
+ *
17
+ * Set `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS=0` to disable the watchdog.
18
+ */
19
+ export function getOpenAIStreamIdleTimeoutMs(): number | undefined {
20
+ return normalizeIdleTimeoutMs($env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS, DEFAULT_OPENAI_STREAM_IDLE_TIMEOUT_MS);
21
+ }
22
+
23
+ /**
24
+ * Returns the timeout used while waiting for the first stream event.
25
+ * The first token can legitimately take longer than later inter-event gaps,
26
+ * so the default never undershoots the steady-state idle timeout.
27
+ *
28
+ * Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog.
29
+ */
30
+ export function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number): number | undefined {
31
+ const fallback =
32
+ idleTimeoutMs === undefined
33
+ ? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS
34
+ : Math.max(DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, idleTimeoutMs);
35
+ return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback);
36
+ }
37
+
38
+ export interface FirstEventWatchdog {
39
+ markFirstEventReceived(): void;
40
+ cleanup(): void;
41
+ }
42
+
43
+ /**
44
+ * Starts a watchdog that aborts a request if no first stream event arrives in time.
45
+ * Call `markFirstEventReceived()` as soon as the first event is observed.
46
+ */
47
+ export function createFirstEventWatchdog(timeoutMs: number | undefined, onTimeout: () => void): FirstEventWatchdog {
48
+ let timer: NodeJS.Timeout | undefined;
49
+ if (timeoutMs !== undefined && timeoutMs > 0) {
50
+ timer = setTimeout(() => {
51
+ timer = undefined;
52
+ onTimeout();
53
+ }, timeoutMs);
54
+ }
55
+ return {
56
+ markFirstEventReceived() {
57
+ if (!timer) return;
58
+ clearTimeout(timer);
59
+ timer = undefined;
60
+ },
61
+ cleanup() {
62
+ if (!timer) return;
63
+ clearTimeout(timer);
64
+ timer = undefined;
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Wraps an async iterable and clears the watchdog once the first event arrives.
71
+ */
72
+ export async function* markFirstStreamEvent<T>(
73
+ iterable: AsyncIterable<T>,
74
+ watchdog: FirstEventWatchdog,
75
+ ): AsyncGenerator<T> {
76
+ let sawFirstEvent = false;
77
+ try {
78
+ for await (const item of iterable) {
79
+ if (!sawFirstEvent) {
80
+ sawFirstEvent = true;
81
+ watchdog.markFirstEventReceived();
82
+ }
83
+ yield item;
84
+ }
85
+ } finally {
86
+ watchdog.cleanup();
87
+ }
88
+ }
89
+
90
+ export interface IdleTimeoutIteratorOptions {
91
+ idleTimeoutMs?: number;
92
+ firstItemTimeoutMs?: number;
93
+ errorMessage: string;
94
+ firstItemErrorMessage?: string;
95
+ onIdle?: () => void;
96
+ onFirstItemTimeout?: () => void;
97
+ }
98
+
99
+ /**
100
+ * Yields items from an async iterable while enforcing a maximum idle gap between items.
101
+ *
102
+ * The first item may use a shorter timeout so stuck requests can be aborted and retried
103
+ * before any user-visible content has streamed.
104
+ */
105
+ export async function* iterateWithIdleTimeout<T>(
106
+ iterable: AsyncIterable<T>,
107
+ options: IdleTimeoutIteratorOptions,
108
+ ): AsyncGenerator<T> {
109
+ const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
110
+ if (
111
+ (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) &&
112
+ (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0)
113
+ ) {
114
+ for await (const item of iterable) {
115
+ yield item;
116
+ }
117
+ return;
118
+ }
119
+
120
+ const iterator = iterable[Symbol.asyncIterator]();
121
+ let sawFirstItem = false;
122
+
123
+ while (true) {
124
+ const nextResultPromise = iterator.next().then(
125
+ result => ({ kind: "next" as const, result }),
126
+ error => ({ kind: "error" as const, error }),
127
+ );
128
+ const activeTimeoutMs = sawFirstItem ? options.idleTimeoutMs : firstItemTimeoutMs;
129
+
130
+ if (activeTimeoutMs === undefined || activeTimeoutMs <= 0) {
131
+ const outcome = await nextResultPromise;
132
+ if (outcome.kind === "error") {
133
+ throw outcome.error;
134
+ }
135
+ if (outcome.result.done) {
136
+ return;
137
+ }
138
+ sawFirstItem = true;
139
+ yield outcome.result.value;
140
+ continue;
141
+ }
142
+
143
+ const { promise: timeoutPromise, resolve: resolveTimeout } = Promise.withResolvers<{
144
+ kind: "timeout";
145
+ }>();
146
+ const timer = setTimeout(() => resolveTimeout({ kind: "timeout" }), activeTimeoutMs);
147
+
148
+ try {
149
+ const outcome = await Promise.race([nextResultPromise, timeoutPromise]);
150
+ if (outcome.kind === "timeout") {
151
+ if (sawFirstItem) {
152
+ options.onIdle?.();
153
+ } else {
154
+ options.onFirstItemTimeout?.();
155
+ }
156
+ const returnPromise = iterator.return?.();
157
+ if (returnPromise) {
158
+ void returnPromise.catch(() => {});
159
+ }
160
+ throw new Error(
161
+ sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage),
162
+ );
163
+ }
164
+ if (outcome.kind === "error") {
165
+ throw outcome.error;
166
+ }
167
+ if (outcome.result.done) {
168
+ return;
169
+ }
170
+ sawFirstItem = true;
171
+ yield outcome.result.value;
172
+ } finally {
173
+ clearTimeout(timer);
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,28 @@
1
+ import { parse as partialParse } from "partial-json";
2
+
3
+ /**
4
+ * Attempts to parse potentially incomplete JSON during streaming.
5
+ * Always returns a valid object, even if the JSON is incomplete.
6
+ *
7
+ * @param partialJson The partial JSON string from streaming
8
+ * @returns Parsed object or empty object if parsing fails
9
+ */
10
+ export function parseStreamingJson<T = any>(partialJson: string | undefined): T {
11
+ if (!partialJson || partialJson.trim() === "") {
12
+ return {} as T;
13
+ }
14
+
15
+ // Try standard parsing first (fastest for complete JSON)
16
+ try {
17
+ return JSON.parse(partialJson) as T;
18
+ } catch {
19
+ // Try partial-json for incomplete JSON
20
+ try {
21
+ const result = partialParse(partialJson);
22
+ return (result ?? {}) as T;
23
+ } catch {
24
+ // If all parsing fails, return empty object
25
+ return {} as T;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Alibaba Coding Plan login flow.
3
+ *
4
+ * Alibaba Coding Plan provides OpenAI-compatible models via https://coding-intl.dashscope.aliyuncs.com/v1.
5
+ *
6
+ * This is not OAuth - it's a simple API key flow:
7
+ * 1. Open browser to Alibaba Cloud DashScope API key settings
8
+ * 2. User copies their API key
9
+ * 3. User pastes the API key into the CLI
10
+ */
11
+
12
+ import { validateOpenAICompatibleApiKey } from "./api-key-validation";
13
+ import type { OAuthController } from "./types";
14
+
15
+ const AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
16
+ const API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
17
+ const VALIDATION_MODEL = "qwen3.5-plus";
18
+
19
+ /**
20
+ * Login to Alibaba Coding Plan.
21
+ *
22
+ * Opens browser to API keys page, prompts user to paste their API key.
23
+ * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
24
+ */
25
+ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<string> {
26
+ if (!options.onPrompt) {
27
+ throw new Error("Alibaba Coding Plan login requires onPrompt callback");
28
+ }
29
+
30
+ options.onAuth?.({
31
+ url: AUTH_URL,
32
+ instructions: "Copy your API key from the Alibaba Cloud DashScope console",
33
+ });
34
+
35
+ const apiKey = await options.onPrompt({
36
+ message: "Paste your Alibaba Coding Plan API key",
37
+ placeholder: "sk-...",
38
+ });
39
+
40
+ if (options.signal?.aborted) {
41
+ throw new Error("Login cancelled");
42
+ }
43
+
44
+ const trimmed = apiKey.trim();
45
+ if (!trimmed) {
46
+ throw new Error("API key is required");
47
+ }
48
+
49
+ options.onProgress?.("Validating API key...");
50
+ await validateOpenAICompatibleApiKey({
51
+ provider: "Alibaba Coding Plan",
52
+ apiKey: trimmed,
53
+ baseUrl: API_BASE_URL,
54
+ model: VALIDATION_MODEL,
55
+ signal: options.signal,
56
+ });
57
+
58
+ return trimmed;
59
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Anthropic OAuth flow (Claude Pro/Max)
3
+ */
4
+ import { OAuthCallbackFlow } from "./callback-server";
5
+ import { generatePKCE } from "./pkce";
6
+ import type { OAuthController, OAuthCredentials } from "./types";
7
+
8
+ const decode = (s: string) => atob(s);
9
+ const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
10
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
11
+ const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
12
+ const CALLBACK_PORT = 54545;
13
+ const CALLBACK_PATH = "/callback";
14
+ const SCOPES = "org:create_api_key user:profile user:inference";
15
+
16
+ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
17
+ #verifier: string = "";
18
+ #challenge: string = "";
19
+
20
+ constructor(ctrl: OAuthController) {
21
+ super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
22
+ }
23
+
24
+ async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
25
+ const pkce = await generatePKCE();
26
+ this.#verifier = pkce.verifier;
27
+ this.#challenge = pkce.challenge;
28
+
29
+ const authParams = new URLSearchParams({
30
+ code: "true",
31
+ client_id: CLIENT_ID,
32
+ response_type: "code",
33
+ redirect_uri: redirectUri,
34
+ scope: SCOPES,
35
+ code_challenge: this.#challenge,
36
+ code_challenge_method: "S256",
37
+ state,
38
+ });
39
+
40
+ const url = `${AUTHORIZE_URL}?${authParams.toString()}`;
41
+ return { url };
42
+ }
43
+
44
+ async exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials> {
45
+ let exchangeCode = code;
46
+ let exchangeState = state;
47
+ const codeFragmentIndex = code.indexOf("#");
48
+ if (codeFragmentIndex >= 0) {
49
+ exchangeCode = code.slice(0, codeFragmentIndex);
50
+ const codeFragmentState = code.slice(codeFragmentIndex + 1);
51
+ if (codeFragmentState.length > 0) {
52
+ exchangeState = codeFragmentState;
53
+ }
54
+ }
55
+
56
+ const tokenResponse = await fetch(TOKEN_URL, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Accept: "application/json",
61
+ },
62
+ body: JSON.stringify({
63
+ grant_type: "authorization_code",
64
+ client_id: CLIENT_ID,
65
+ code: exchangeCode,
66
+ state: exchangeState,
67
+ redirect_uri: redirectUri,
68
+ code_verifier: this.#verifier,
69
+ }),
70
+ });
71
+
72
+ if (!tokenResponse.ok) {
73
+ let error: string;
74
+ try {
75
+ error = await tokenResponse.text();
76
+ } catch {
77
+ error = `HTTP ${tokenResponse.status}`;
78
+ }
79
+ throw new Error(`Token exchange failed: ${error}`);
80
+ }
81
+
82
+ const tokenData = (await tokenResponse.json()) as {
83
+ access_token: string;
84
+ refresh_token: string;
85
+ expires_in: number;
86
+ };
87
+
88
+ return {
89
+ refresh: tokenData.refresh_token,
90
+ access: tokenData.access_token,
91
+ expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
92
+ };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Login with Anthropic OAuth
98
+ */
99
+ export async function loginAnthropic(ctrl: OAuthController): Promise<OAuthCredentials> {
100
+ const flow = new AnthropicOAuthFlow(ctrl);
101
+ return flow.login();
102
+ }
103
+
104
+ /**
105
+ * Refresh Anthropic OAuth token
106
+ */
107
+ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
108
+ const response = await fetch(TOKEN_URL, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
111
+ body: JSON.stringify({
112
+ grant_type: "refresh_token",
113
+ client_id: CLIENT_ID,
114
+ refresh_token: refreshToken,
115
+ }),
116
+ });
117
+
118
+ if (!response.ok) {
119
+ const error = await response.text();
120
+ throw new Error(`Anthropic token refresh failed: ${error}`);
121
+ }
122
+
123
+ const data = (await response.json()) as {
124
+ access_token: string;
125
+ refresh_token: string;
126
+ expires_in: number;
127
+ };
128
+
129
+ return {
130
+ refresh: data.refresh_token || refreshToken,
131
+ access: data.access_token,
132
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
133
+ };
134
+ }
@@ -0,0 +1,92 @@
1
+ type OpenAICompatibleValidationOptions = {
2
+ provider: string;
3
+ apiKey: string;
4
+ baseUrl: string;
5
+ model: string;
6
+ signal?: AbortSignal;
7
+ };
8
+
9
+ type ModelListValidationOptions = {
10
+ provider: string;
11
+ apiKey: string;
12
+ modelsUrl: string;
13
+ signal?: AbortSignal;
14
+ };
15
+
16
+ const VALIDATION_TIMEOUT_MS = 15_000;
17
+
18
+ /**
19
+ * Validate an API key against an OpenAI-compatible chat completions endpoint.
20
+ *
21
+ * Performs a minimal request to verify credentials and endpoint access.
22
+ */
23
+ export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleValidationOptions): Promise<void> {
24
+ const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
25
+ const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
26
+
27
+ const response = await fetch(`${options.baseUrl}/chat/completions`, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ Authorization: `Bearer ${options.apiKey}`,
32
+ },
33
+ body: JSON.stringify({
34
+ model: options.model,
35
+ messages: [{ role: "user", content: "ping" }],
36
+ max_tokens: 1,
37
+ temperature: 0,
38
+ }),
39
+ signal,
40
+ });
41
+
42
+ if (response.ok) {
43
+ return;
44
+ }
45
+
46
+ let details = "";
47
+ try {
48
+ details = (await response.text()).trim();
49
+ } catch {
50
+ // ignore body parse errors, status is enough
51
+ }
52
+
53
+ const message = details
54
+ ? `${options.provider} API key validation failed (${response.status}): ${details}`
55
+ : `${options.provider} API key validation failed (${response.status})`;
56
+ throw new Error(message);
57
+ }
58
+
59
+ /**
60
+ * Validate an API key against a provider models endpoint.
61
+ *
62
+ * Useful for providers where access to specific models may vary by plan and
63
+ * should not block key validation.
64
+ */
65
+ export async function validateApiKeyAgainstModelsEndpoint(options: ModelListValidationOptions): Promise<void> {
66
+ const timeoutSignal = AbortSignal.timeout(VALIDATION_TIMEOUT_MS);
67
+ const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
68
+
69
+ const response = await fetch(options.modelsUrl, {
70
+ method: "GET",
71
+ headers: {
72
+ Authorization: `Bearer ${options.apiKey}`,
73
+ },
74
+ signal,
75
+ });
76
+
77
+ if (response.ok) {
78
+ return;
79
+ }
80
+
81
+ let details = "";
82
+ try {
83
+ details = (await response.text()).trim();
84
+ } catch {
85
+ // ignore body parse errors, status is enough
86
+ }
87
+
88
+ const message = details
89
+ ? `${options.provider} API key validation failed (${response.status}): ${details}`
90
+ : `${options.provider} API key validation failed (${response.status})`;
91
+ throw new Error(message);
92
+ }