@oh-my-pi/pi-ai 16.1.13 → 16.1.14

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.
@@ -13,8 +13,7 @@
13
13
  * China: https://api.minimaxi.com/v1
14
14
  */
15
15
 
16
- import { validateOpenAICompatibleApiKey } from "../api-key-validation";
17
- import type { OAuthController } from "./types";
16
+ import { createApiKeyLogin } from "../api-key-login";
18
17
 
19
18
  const AUTH_URL_INTL = "https://platform.minimax.io/subscribe/token-plan";
20
19
  const AUTH_URL_CN = "https://platform.minimaxi.com/subscribe/token-plan";
@@ -22,61 +21,33 @@ const API_BASE_URL_INTL = "https://api.minimax.io/v1";
22
21
  const API_BASE_URL_CN = "https://api.minimaxi.com/v1";
23
22
  const VALIDATION_MODEL = "MiniMax-M3";
24
23
 
24
+ function createMiniMaxLogin(authUrl: string, baseUrl: string, provider: string) {
25
+ return createApiKeyLogin({
26
+ providerLabel: "MiniMax Token Plan",
27
+ authUrl,
28
+ instructions: "Subscribe to Token Plan and copy your API key",
29
+ promptMessage: "Paste your MiniMax Token Plan API key",
30
+ placeholder: "sk-...",
31
+ validation: {
32
+ kind: "chat-completions",
33
+ provider,
34
+ baseUrl,
35
+ model: VALIDATION_MODEL,
36
+ },
37
+ });
38
+ }
39
+
25
40
  /**
26
41
  * Login to MiniMax Token Plan (international).
27
42
  *
28
43
  * Opens browser to subscription page, prompts user to paste their API key.
29
44
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
30
45
  */
31
- export async function loginMiniMaxCode(options: OAuthController): Promise<string> {
32
- return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_INTL, API_BASE_URL_INTL, "MiniMax Token Plan");
33
- }
34
-
35
- async function loginMiniMaxCodeWithBaseUrl(
36
- options: OAuthController,
37
- authUrl: string,
38
- baseUrl: string,
39
- providerName: string,
40
- ): Promise<string> {
41
- const fetchImpl = options.fetch ?? fetch;
42
- if (!options.onPrompt) {
43
- throw new Error("MiniMax Token Plan login requires onPrompt callback");
44
- }
45
- // Open browser to subscription page
46
- options.onAuth?.({
47
- url: authUrl,
48
- instructions: "Subscribe to Token Plan and copy your API key",
49
- });
50
- // Prompt user to paste their API key
51
- const apiKey = await options.onPrompt({
52
- message: "Paste your MiniMax Token Plan API key",
53
- placeholder: "sk-...",
54
- });
55
- if (options.signal?.aborted) {
56
- throw new Error("Login cancelled");
57
- }
58
- const trimmed = apiKey.trim();
59
- if (!trimmed) {
60
- throw new Error("API key is required");
61
- }
62
-
63
- options.onProgress?.("Validating API key...");
64
- await validateOpenAICompatibleApiKey({
65
- provider: providerName,
66
- apiKey: trimmed,
67
- baseUrl,
68
- model: VALIDATION_MODEL,
69
- signal: options.signal,
70
- fetch: fetchImpl,
71
- });
72
- return trimmed;
73
- }
46
+ export const loginMiniMaxCode = createMiniMaxLogin(AUTH_URL_INTL, API_BASE_URL_INTL, "MiniMax Token Plan");
74
47
 
75
48
  /**
76
49
  * Login to MiniMax Token Plan (China).
77
50
  *
78
51
  * Same flow as international but uses China endpoint.
79
52
  */
80
- export async function loginMiniMaxCodeCn(options: OAuthController): Promise<string> {
81
- return loginMiniMaxCodeWithBaseUrl(options, AUTH_URL_CN, API_BASE_URL_CN, "MiniMax Token Plan (China)");
82
- }
53
+ export const loginMiniMaxCodeCn = createMiniMaxLogin(AUTH_URL_CN, API_BASE_URL_CN, "MiniMax Token Plan (China)");
@@ -1,46 +1,24 @@
1
- import { validateOpenAICompatibleApiKey } from "./api-key-validation";
2
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
3
  import type { ProviderDefinition } from "./types";
4
4
 
5
5
  const AUTH_URL = "https://console.bce.baidu.com/qianfan/ais/console/apiKey";
6
6
  const API_BASE_URL = "https://qianfan.baidubce.com/v2";
7
7
  const VALIDATION_MODEL = "deepseek-v3.2";
8
8
 
9
- export async function loginQianfan(options: OAuthController): Promise<string> {
10
- if (!options.onPrompt) {
11
- throw new Error("Qianfan login requires onPrompt callback");
12
- }
13
-
14
- options.onAuth?.({
15
- url: AUTH_URL,
16
- instructions: "Copy your Qianfan API key from the console",
17
- });
18
-
19
- const apiKey = await options.onPrompt({
20
- message: "Paste your Qianfan API key",
21
- placeholder: "bce-v3/ALTAK-...",
22
- });
23
-
24
- if (options.signal?.aborted) {
25
- throw new Error("Login cancelled");
26
- }
27
-
28
- const trimmed = apiKey.trim();
29
- if (!trimmed) {
30
- throw new Error("API key is required");
31
- }
32
-
33
- options.onProgress?.("Validating API key...");
34
- await validateOpenAICompatibleApiKey({
9
+ export const loginQianfan = createApiKeyLogin({
10
+ providerLabel: "Qianfan",
11
+ authUrl: AUTH_URL,
12
+ instructions: "Copy your Qianfan API key from the console",
13
+ promptMessage: "Paste your Qianfan API key",
14
+ placeholder: "bce-v3/ALTAK-...",
15
+ validation: {
16
+ kind: "chat-completions",
35
17
  provider: "qianfan",
36
- apiKey: trimmed,
37
18
  baseUrl: API_BASE_URL,
38
19
  model: VALIDATION_MODEL,
39
- signal: options.signal,
40
- });
41
-
42
- return trimmed;
43
- }
20
+ },
21
+ });
44
22
 
45
23
  export const qianfanProvider = {
46
24
  id: "qianfan",
@@ -44,6 +44,7 @@ import { parallelProvider } from "./parallel";
44
44
  import { perplexityProvider } from "./perplexity";
45
45
  import { qianfanProvider } from "./qianfan";
46
46
  import { qwenPortalProvider } from "./qwen-portal";
47
+ import { sakanaProvider } from "./sakana";
47
48
  import { syntheticProvider } from "./synthetic";
48
49
  import { tavilyProvider } from "./tavily";
49
50
  import { togetherProvider } from "./together";
@@ -90,6 +91,7 @@ const ALL = [
90
91
  zhipuCodingPlanProvider,
91
92
  umansProvider,
92
93
  qwenPortalProvider,
94
+ sakanaProvider,
93
95
  minimaxCodeProvider,
94
96
  minimaxCodeCnProvider,
95
97
  xiaomiProvider,
@@ -0,0 +1,22 @@
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
+ import type { ProviderDefinition } from "./types";
4
+
5
+ export const loginSakana = createApiKeyLogin({
6
+ providerLabel: "Sakana AI",
7
+ authUrl: "https://console.sakana.ai/api-keys",
8
+ instructions: "Copy your API key from the Sakana AI console",
9
+ promptMessage: "Paste your Sakana AI API key",
10
+ placeholder: "sk-...",
11
+ validation: {
12
+ kind: "models-endpoint",
13
+ provider: "Sakana AI",
14
+ modelsUrl: "https://api.sakana.ai/v1/models",
15
+ },
16
+ });
17
+
18
+ export const sakanaProvider = {
19
+ id: "sakana",
20
+ name: "Sakana AI",
21
+ login: (cb: OAuthLoginCallbacks) => loginSakana(cb),
22
+ } as const satisfies ProviderDefinition;
@@ -1,5 +1,5 @@
1
- import { validateOpenAICompatibleApiKey } from "./api-key-validation";
2
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
3
  import type { ProviderDefinition } from "./types";
4
4
 
5
5
  const AUTH_URL = "https://venice.ai/settings/api";
@@ -12,41 +12,19 @@ const VALIDATION_MODEL = "qwen3-4b";
12
12
  * Opens browser to API keys page, prompts user to paste their API key.
13
13
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
14
14
  */
15
- export async function loginVenice(options: OAuthController): Promise<string> {
16
- if (!options.onPrompt) {
17
- throw new Error("Venice login requires onPrompt callback");
18
- }
19
-
20
- options.onAuth?.({
21
- url: AUTH_URL,
22
- instructions: "Copy your API key from the Venice dashboard",
23
- });
24
-
25
- const apiKey = await options.onPrompt({
26
- message: "Paste your Venice API key",
27
- placeholder: "vapi_...",
28
- });
29
-
30
- if (options.signal?.aborted) {
31
- throw new Error("Login cancelled");
32
- }
33
-
34
- const trimmed = apiKey.trim();
35
- if (!trimmed) {
36
- throw new Error("API key is required");
37
- }
38
-
39
- options.onProgress?.("Validating API key...");
40
- await validateOpenAICompatibleApiKey({
15
+ export const loginVenice = createApiKeyLogin({
16
+ providerLabel: "Venice",
17
+ authUrl: AUTH_URL,
18
+ instructions: "Copy your API key from the Venice dashboard",
19
+ promptMessage: "Paste your Venice API key",
20
+ placeholder: "vapi_...",
21
+ validation: {
22
+ kind: "chat-completions",
41
23
  provider: "Venice",
42
- apiKey: trimmed,
43
24
  baseUrl: API_BASE_URL,
44
25
  model: VALIDATION_MODEL,
45
- signal: options.signal,
46
- });
47
-
48
- return trimmed;
49
- }
26
+ },
27
+ });
50
28
 
51
29
  export const veniceProvider = {
52
30
  id: "venice",
@@ -1,45 +1,24 @@
1
- import { validateOpenAICompatibleApiKey } from "./api-key-validation";
2
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
3
  import type { ProviderDefinition } from "./types";
4
4
 
5
5
  const AUTH_URL = "https://z.ai/manage-apikey/apikey-list";
6
6
  const API_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
7
7
  const VALIDATION_MODEL = "glm-5.2";
8
8
 
9
- export async function loginZai(options: OAuthController): Promise<string> {
10
- if (!options.onPrompt) {
11
- throw new Error("Z.AI login requires onPrompt callback");
12
- }
13
-
14
- options.onAuth?.({
15
- url: AUTH_URL,
16
- instructions: "Copy your API key from the dashboard",
17
- });
18
-
19
- const apiKey = await options.onPrompt({
20
- message: "Paste your Z.AI API key",
21
- placeholder: "sk-...",
22
- });
23
-
24
- if (options.signal?.aborted) {
25
- throw new Error("Login cancelled");
26
- }
27
-
28
- const trimmed = apiKey.trim();
29
- if (!trimmed) {
30
- throw new Error("API key is required");
31
- }
32
-
33
- options.onProgress?.("Validating API key...");
34
- await validateOpenAICompatibleApiKey({
9
+ export const loginZai = createApiKeyLogin({
10
+ providerLabel: "Z.AI",
11
+ authUrl: AUTH_URL,
12
+ instructions: "Copy your API key from the dashboard",
13
+ promptMessage: "Paste your Z.AI API key",
14
+ placeholder: "sk-...",
15
+ validation: {
16
+ kind: "chat-completions",
35
17
  provider: "Z.AI",
36
- apiKey: trimmed,
37
18
  baseUrl: API_BASE_URL,
38
19
  model: VALIDATION_MODEL,
39
- signal: options.signal,
40
- });
41
- return trimmed;
42
- }
20
+ },
21
+ });
43
22
 
44
23
  export const zaiProvider = {
45
24
  id: "zai",
@@ -1,45 +1,24 @@
1
- import { validateOpenAICompatibleApiKey } from "./api-key-validation";
2
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
3
  import type { ProviderDefinition } from "./types";
4
4
 
5
5
  const AUTH_URL = "https://bigmodel.cn/coding-plan/personal/overview";
6
6
  const API_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4";
7
7
  const VALIDATION_MODEL = "glm-5.1";
8
8
 
9
- export async function loginZhipuCodingPlan(options: OAuthController): Promise<string> {
10
- if (!options.onPrompt) {
11
- throw new Error("Zhipu Coding Plan login requires onPrompt callback");
12
- }
13
-
14
- options.onAuth?.({
15
- url: AUTH_URL,
16
- instructions: "Copy your API key from the Coding Plan dashboard",
17
- });
18
-
19
- const apiKey = await options.onPrompt({
20
- message: "Paste your Zhipu API key",
21
- placeholder: "<id>.<secret>",
22
- });
23
-
24
- if (options.signal?.aborted) {
25
- throw new Error("Login cancelled");
26
- }
27
-
28
- const trimmed = apiKey.trim();
29
- if (!trimmed) {
30
- throw new Error("API key is required");
31
- }
32
-
33
- options.onProgress?.("Validating API key...");
34
- await validateOpenAICompatibleApiKey({
9
+ export const loginZhipuCodingPlan = createApiKeyLogin({
10
+ providerLabel: "Zhipu Coding Plan",
11
+ authUrl: AUTH_URL,
12
+ instructions: "Copy your API key from the Coding Plan dashboard",
13
+ promptMessage: "Paste your Zhipu API key",
14
+ placeholder: "<id>.<secret>",
15
+ validation: {
16
+ kind: "chat-completions",
35
17
  provider: "Zhipu",
36
- apiKey: trimmed,
37
18
  baseUrl: API_BASE_URL,
38
19
  model: VALIDATION_MODEL,
39
- signal: options.signal,
40
- });
41
- return trimmed;
42
- }
20
+ },
21
+ });
43
22
 
44
23
  export const zhipuCodingPlanProvider = {
45
24
  id: "zhipu-coding-plan",
package/src/stream.ts CHANGED
@@ -64,6 +64,7 @@ import type {
64
64
  ToolChoice,
65
65
  } from "./types";
66
66
  import { AssistantMessageEventStream } from "./utils/event-stream";
67
+ import { wrapFetchForProxy } from "./utils/proxy";
67
68
  import { withRequestDebugFetch } from "./utils/request-debug";
68
69
  import { withGeminiThinkingLoopGuard } from "./utils/thinking-loop";
69
70
 
@@ -237,9 +238,12 @@ function streamDispatch<TApi extends Api>(
237
238
  context: Context,
238
239
  options?: OptionsForApi<TApi>,
239
240
  ): AssistantMessageEventStream {
240
- const requestOptions = withRequestDebugFetch(options as StreamOptions | undefined) as
241
- | OptionsForApi<TApi>
242
- | undefined;
241
+ const baseOptions = (options || {}) as StreamOptions;
242
+ const debugOptions = withRequestDebugFetch(baseOptions);
243
+ const requestOptions = {
244
+ ...debugOptions,
245
+ fetch: wrapFetchForProxy(debugOptions.fetch ?? (globalThis.fetch as FetchImpl), model.provider),
246
+ } as OptionsForApi<TApi>;
243
247
 
244
248
  // Check custom API registry first (extension-provided APIs like "vertex-claude-api")
245
249
  const customApiProvider = getCustomApi(model.api);
@@ -248,12 +252,12 @@ function streamDispatch<TApi extends Api>(
248
252
  }
249
253
 
250
254
  if (isGitLabDuoModel(model)) {
251
- const apiKey = (requestOptions as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
255
+ const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
252
256
  if (!apiKey) {
253
257
  throw new Error(`No API key for provider: ${model.provider}`);
254
258
  }
255
259
  return streamGitLabDuo(model, context, {
256
- ...(requestOptions as SimpleStreamOptions | undefined),
260
+ ...(requestOptions as SimpleStreamOptions),
257
261
  apiKey,
258
262
  });
259
263
  }
@@ -263,14 +267,10 @@ function streamDispatch<TApi extends Api>(
263
267
  return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
264
268
  } else if (model.api === "bedrock-converse-stream") {
265
269
  // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
266
- return streamBedrock(
267
- model as Model<"bedrock-converse-stream">,
268
- context,
269
- (requestOptions || {}) as BedrockOptions,
270
- );
270
+ return streamBedrock(model as Model<"bedrock-converse-stream">, context, requestOptions as BedrockOptions);
271
271
  }
272
272
 
273
- const apiKey = requestOptions?.apiKey || getEnvApiKey(model.provider);
273
+ const apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
274
274
  if (!apiKey) {
275
275
  throw new Error(`No API key for provider: ${model.provider}`);
276
276
  }
@@ -278,7 +278,7 @@ function streamDispatch<TApi extends Api>(
278
278
  ? {
279
279
  ...requestOptions,
280
280
  apiKey: "vertex-adc",
281
- fetch: createVertexAuthenticatedFetch(requestOptions as StreamOptions | undefined),
281
+ fetch: createVertexAuthenticatedFetch(requestOptions),
282
282
  }
283
283
  : { ...requestOptions, apiKey };
284
284
 
@@ -410,7 +410,12 @@ export function streamSimple<TApi extends Api>(
410
410
  context: Context,
411
411
  options?: SimpleStreamOptions,
412
412
  ): AssistantMessageEventStream {
413
- const requestOptions = withRequestDebugFetch(options);
413
+ const baseOptions = (options || {}) as SimpleStreamOptions;
414
+ const debugOptions = withRequestDebugFetch(baseOptions);
415
+ const requestOptions = {
416
+ ...debugOptions,
417
+ fetch: wrapFetchForProxy(debugOptions.fetch ?? (globalThis.fetch as FetchImpl), model.provider),
418
+ } as SimpleStreamOptions;
414
419
  const apiKeyResolver = isApiKeyResolver(requestOptions?.apiKey) ? requestOptions.apiKey : undefined;
415
420
  if (apiKeyResolver) {
416
421
  const outer = new AssistantMessageEventStream();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A UUID-shaped string: five hex groups in the canonical 8-4-4-4-12 layout.
3
+ *
4
+ * NOT a spec-compliant RFC 4122 UUID — the version/variant nibbles are left as
5
+ * raw hash output — but the shape passes everywhere a UUID string is expected.
6
+ */
7
+ export type DeterministicUuid = `${string}-${string}-${string}-${string}-${string}`;
8
+
9
+ /**
10
+ * Format the leading 128 bits of `seed`'s SHA-256 digest as a v4-shape UUID
11
+ * (8-4-4-4-12 hex groups).
12
+ *
13
+ * Deterministic: identical seeds always map to the same id, so callers get
14
+ * stable ids across requests / conversation turns (reusing message-blob ids,
15
+ * keying prompt caches) without persisting a seed→id mapping.
16
+ */
17
+ export function deterministicUuid(seed: string): DeterministicUuid {
18
+ const hex = new Bun.CryptoHasher("sha256").update(seed).digest("hex");
19
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
20
+ }