@oh-my-pi/pi-ai 17.1.5 → 17.1.6

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.6] - 2026-07-27
6
+
7
+ ### Added
8
+
9
+ - Added `getProxyForUrl()` for transports that need provider-specific and standard proxy environment resolution with `NO_PROXY` support ([#6770](https://github.com/can1357/oh-my-pi/issues/6770)).
10
+ - Added SiliconFlow and SiliconFlow (China) to the built-in API-key login provider catalog so `omp login siliconflow` / `omp login siliconflow-cn` stores a reusable credential validated against each region's `/v1/models` endpoint.
11
+
5
12
  ## [17.1.5] - 2026-07-27
6
13
 
7
14
  ### Fixed
@@ -239,6 +239,14 @@ declare const ALL: ({
239
239
  readonly id: "sakana";
240
240
  readonly name: "Sakana AI";
241
241
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
242
+ } | {
243
+ readonly id: "siliconflow";
244
+ readonly name: "SiliconFlow";
245
+ readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
246
+ } | {
247
+ readonly id: "siliconflow-cn";
248
+ readonly name: "SiliconFlow (China)";
249
+ readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
242
250
  } | {
243
251
  readonly id: "synthetic";
244
252
  readonly name: "Synthetic";
@@ -0,0 +1,7 @@
1
+ import type { OAuthLoginCallbacks } from "./oauth/types.js";
2
+ export declare const loginSiliconFlowCn: (options: import("./oauth/index.js").OAuthController) => Promise<string>;
3
+ export declare const siliconflowCnProvider: {
4
+ readonly id: "siliconflow-cn";
5
+ readonly name: "SiliconFlow (China)";
6
+ readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
7
+ };
@@ -0,0 +1,7 @@
1
+ import type { OAuthLoginCallbacks } from "./oauth/types.js";
2
+ export declare const loginSiliconFlow: (options: import("./oauth/index.js").OAuthController) => Promise<string>;
3
+ export declare const siliconflowProvider: {
4
+ readonly id: "siliconflow";
5
+ readonly name: "SiliconFlow";
6
+ readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
7
+ };
@@ -18,6 +18,8 @@ export declare function __resetProxyCache(): void;
18
18
  * for the lifetime of the process and this function is called for every outgoing request.
19
19
  */
20
20
  export declare function getProxyForProvider(provider: string): string | undefined;
21
+ /** Resolves provider-specific and standard proxy variables for a target URL, honoring NO_PROXY. */
22
+ export declare function getProxyForUrl(provider: string, url: URL): string | undefined;
21
23
  /**
22
24
  * Wraps a fetch implementation to inject proxy options for non-local hosts.
23
25
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.1.5",
4
+ "version": "17.1.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "17.1.5",
42
- "@oh-my-pi/pi-utils": "17.1.5",
43
- "@oh-my-pi/pi-wire": "17.1.5",
41
+ "@oh-my-pi/pi-catalog": "17.1.6",
42
+ "@oh-my-pi/pi-utils": "17.1.6",
43
+ "@oh-my-pi/pi-wire": "17.1.6",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -61,7 +61,7 @@ import {
61
61
  getOpenAIStreamIdleTimeoutMs,
62
62
  iterateWithIdleTimeout,
63
63
  } from "../utils/idle-iterator";
64
- import { getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
64
+ import { getProxyForUrl } from "../utils/proxy";
65
65
  import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
66
66
  import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
67
67
  import { notifyRawSseEvent } from "../utils/sse-debug";
@@ -3855,15 +3855,7 @@ async function getOrCreateCodexWebSocketConnection(
3855
3855
  provider: string,
3856
3856
  signal?: AbortSignal,
3857
3857
  ): Promise<CodexWebSocketConnection> {
3858
- const targetUrl = new URL(url);
3859
- const proxy = shouldBypassProxy(targetUrl)
3860
- ? undefined
3861
- : (getProxyForProvider(provider) ??
3862
- (targetUrl.protocol === "wss:"
3863
- ? Bun.env.HTTPS_PROXY || Bun.env.https_proxy
3864
- : Bun.env.HTTP_PROXY || Bun.env.http_proxy) ??
3865
- Bun.env.ALL_PROXY ??
3866
- Bun.env.all_proxy);
3858
+ const proxy = getProxyForUrl(provider, new URL(url));
3867
3859
  const headerRecord = headersToRecord(headers);
3868
3860
  // Join an in-flight handshake instead of tearing it down: closing a
3869
3861
  // CONNECTING socket rejects the concurrent caller (prewarm racing the first
@@ -51,6 +51,8 @@ import { perplexityProvider } from "./perplexity";
51
51
  import { qianfanProvider } from "./qianfan";
52
52
  import { qwenPortalProvider } from "./qwen-portal";
53
53
  import { sakanaProvider } from "./sakana";
54
+ import { siliconflowProvider } from "./siliconflow";
55
+ import { siliconflowCnProvider } from "./siliconflow-cn";
54
56
  import { syntheticProvider } from "./synthetic";
55
57
  import { tavilyProvider } from "./tavily";
56
58
  import { togetherProvider } from "./together";
@@ -121,6 +123,8 @@ const ALL = [
121
123
  perplexityProvider,
122
124
  qianfanProvider,
123
125
  veniceProvider,
126
+ siliconflowProvider,
127
+ siliconflowCnProvider,
124
128
  syntheticProvider,
125
129
  nanogptProvider,
126
130
  waferServerlessProvider,
@@ -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 loginSiliconFlowCn = createApiKeyLogin({
6
+ providerLabel: "SiliconFlow (China)",
7
+ authUrl: "https://cloud.siliconflow.cn/account/ak",
8
+ instructions: "Create or copy your API key from the SiliconFlow console",
9
+ promptMessage: "Paste your SiliconFlow API key",
10
+ placeholder: "sk-...",
11
+ validation: {
12
+ kind: "models-endpoint",
13
+ provider: "siliconflow-cn",
14
+ modelsUrl: "https://api.siliconflow.cn/v1/models",
15
+ },
16
+ });
17
+
18
+ export const siliconflowCnProvider = {
19
+ id: "siliconflow-cn",
20
+ name: "SiliconFlow (China)",
21
+ login: (cb: OAuthLoginCallbacks) => loginSiliconFlowCn(cb),
22
+ } as const satisfies ProviderDefinition;
@@ -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 loginSiliconFlow = createApiKeyLogin({
6
+ providerLabel: "SiliconFlow",
7
+ authUrl: "https://cloud.siliconflow.com/account/ak",
8
+ instructions: "Create or copy your API key from the SiliconFlow console",
9
+ promptMessage: "Paste your SiliconFlow API key",
10
+ placeholder: "sk-...",
11
+ validation: {
12
+ kind: "models-endpoint",
13
+ provider: "siliconflow",
14
+ modelsUrl: "https://api.siliconflow.com/v1/models",
15
+ },
16
+ });
17
+
18
+ export const siliconflowProvider = {
19
+ id: "siliconflow",
20
+ name: "SiliconFlow",
21
+ login: (cb: OAuthLoginCallbacks) => loginSiliconFlow(cb),
22
+ } as const satisfies ProviderDefinition;
@@ -129,6 +129,16 @@ export function getProxyForProvider(provider: string): string | undefined {
129
129
  return value;
130
130
  }
131
131
 
132
+ /** Resolves provider-specific and standard proxy variables for a target URL, honoring NO_PROXY. */
133
+ export function getProxyForUrl(provider: string, url: URL): string | undefined {
134
+ if (shouldBypassProxy(url)) return undefined;
135
+ const protocolProxy =
136
+ url.protocol === "https:" || url.protocol === "wss:"
137
+ ? Bun.env.HTTPS_PROXY || Bun.env.https_proxy
138
+ : Bun.env.HTTP_PROXY || Bun.env.http_proxy;
139
+ return getProxyForProvider(provider) || protocolProxy || Bun.env.ALL_PROXY || Bun.env.all_proxy || undefined;
140
+ }
141
+
132
142
  /**
133
143
  * Wraps a fetch implementation to inject proxy options for non-local hosts.
134
144
  */
@@ -1 +0,0 @@
1
- export {};
@@ -1,144 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { Buffer } from "node:buffer";
3
- import * as fs from "node:fs/promises";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- import type { FetchImpl } from "../../types";
7
- import { __resetVertexTokenCache, getVertexAccessToken } from "../google-auth";
8
-
9
- const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
10
- const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
11
-
12
- /** Generate a real RS256 private key so signJwtRs256 / pemToPkcs8 run for real. */
13
- async function generateServiceAccountPem(): Promise<string> {
14
- const keyPair = (await globalThis.crypto.subtle.generateKey(
15
- { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
16
- true,
17
- ["sign", "verify"],
18
- )) as CryptoKeyPair;
19
- const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey));
20
- const body = (
21
- Buffer.from(pkcs8)
22
- .toString("base64")
23
- .match(/.{1,64}/g) ?? []
24
- ).join("\n");
25
- return `-----BEGIN PRIVATE KEY-----\n${body}\n-----END PRIVATE KEY-----\n`;
26
- }
27
-
28
- function urlOf(input: string | URL | Request): string {
29
- if (typeof input === "string") return input;
30
- if (input instanceof URL) return input.toString();
31
- return input.url;
32
- }
33
-
34
- describe("getVertexAccessToken impersonated_service_account ADC", () => {
35
- let tmpDir: string;
36
- let originalGac: string | undefined;
37
-
38
- beforeEach(async () => {
39
- __resetVertexTokenCache();
40
- originalGac = Bun.env.GOOGLE_APPLICATION_CREDENTIALS;
41
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-vertex-adc-"));
42
- });
43
-
44
- afterEach(async () => {
45
- __resetVertexTokenCache();
46
- if (originalGac === undefined) delete Bun.env.GOOGLE_APPLICATION_CREDENTIALS;
47
- else Bun.env.GOOGLE_APPLICATION_CREDENTIALS = originalGac;
48
- await fs.rm(tmpDir, { recursive: true, force: true });
49
- });
50
-
51
- it("rejects a malformed service_account_impersonation_url before any network call", async () => {
52
- const adcPath = path.join(tmpDir, "impersonated-bad-url.json");
53
- await Bun.write(
54
- adcPath,
55
- JSON.stringify({
56
- type: "impersonated_service_account",
57
- // Missing the trailing ":generateAccessToken" the principal parser requires.
58
- service_account_impersonation_url:
59
- "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com",
60
- source_credentials: {
61
- type: "authorized_user",
62
- client_id: "client-id",
63
- client_secret: "client-secret",
64
- refresh_token: "refresh-token",
65
- },
66
- }),
67
- );
68
- Bun.env.GOOGLE_APPLICATION_CREDENTIALS = adcPath;
69
-
70
- const calls: string[] = [];
71
- const fetchImpl: FetchImpl = async input => {
72
- calls.push(urlOf(input));
73
- return new Response("{}");
74
- };
75
-
76
- // The principal is parsed before the source exchange, so a bad URL must fail
77
- // up front rather than after burning a source-token round trip.
78
- await expect(getVertexAccessToken({ fetch: fetchImpl })).rejects.toBeInstanceOf(RangeError);
79
- expect(calls).toEqual([]);
80
- });
81
-
82
- it("signs an RS256 JWT for a service_account source and reconstructs the IAM URL", async () => {
83
- const pem = await generateServiceAccountPem();
84
- const adcPath = path.join(tmpDir, "impersonated-sa.json");
85
- await Bun.write(
86
- adcPath,
87
- JSON.stringify({
88
- type: "impersonated_service_account",
89
- // Non-canonical project segment proves the request URL is rebuilt, not echoed.
90
- service_account_impersonation_url:
91
- "https://iamcredentials.googleapis.com/v1/projects/explicit-proj/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken",
92
- source_credentials: {
93
- type: "service_account",
94
- client_email: "source@project.iam.gserviceaccount.com",
95
- private_key: pem,
96
- private_key_id: "key-1",
97
- },
98
- // delegates intentionally omitted — the IAM body must default to [].
99
- }),
100
- );
101
- Bun.env.GOOGLE_APPLICATION_CREDENTIALS = adcPath;
102
-
103
- const calls: { url: string; init?: RequestInit }[] = [];
104
- const fetchImpl: FetchImpl = async (input, init) => {
105
- const url = urlOf(input);
106
- calls.push({ url, init });
107
- if (url === "https://oauth2.googleapis.com/token") {
108
- return new Response(JSON.stringify({ access_token: "sa-source-token", expires_in: 3600 }));
109
- }
110
- if (url.startsWith("https://iamcredentials.googleapis.com/")) {
111
- return new Response(
112
- JSON.stringify({
113
- accessToken: "impersonated-token",
114
- expireTime: new Date(Date.now() + 3_600_000).toISOString(),
115
- }),
116
- );
117
- }
118
- return new Response("unexpected", { status: 404 });
119
- };
120
-
121
- const token = await getVertexAccessToken({ fetch: fetchImpl });
122
- expect(token).toBe("impersonated-token");
123
-
124
- // Source JWT exchange happens first, then the impersonation exchange.
125
- expect(calls.map(c => c.url)).toEqual([
126
- "https://oauth2.googleapis.com/token",
127
- "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target@project.iam.gserviceaccount.com:generateAccessToken",
128
- ]);
129
-
130
- // Source credential is exchanged via a signed JWT bearer assertion, not a refresh grant.
131
- const sourceBody = new URLSearchParams(String(calls[0].init?.body));
132
- expect(sourceBody.get("grant_type")).toBe(JWT_BEARER_GRANT);
133
- expect((sourceBody.get("assertion") ?? "").split(".")).toHaveLength(3);
134
-
135
- // The IAM call carries the source-derived bearer token and defaults delegates to [].
136
- const iamHeaders = calls[1].init?.headers as Record<string, string>;
137
- expect(iamHeaders.Authorization).toBe("Bearer sa-source-token");
138
- expect(JSON.parse(String(calls[1].init?.body))).toEqual({
139
- delegates: [],
140
- scope: [CLOUD_PLATFORM_SCOPE],
141
- lifetime: "3600s",
142
- });
143
- });
144
- });
@@ -1,39 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { NO_AUTH_SENTINEL, resolveOpenAIRequestSetup } from "../openai-shared";
3
-
4
- describe("resolveOpenAIRequestSetup keyless auth", () => {
5
- test("omits Authorization for the keyless (auth: none) sentinel, keeping custom headers", () => {
6
- const setup = resolveOpenAIRequestSetup(
7
- {
8
- provider: "qwen",
9
- id: "Qwen3.6-35B-A3B",
10
- baseUrl: "http://localhost:8788",
11
- headers: { "x-api-key": "real-key" },
12
- },
13
- { apiKey: NO_AUTH_SENTINEL, messages: [] },
14
- );
15
- expect(setup.headers.Authorization).toBeUndefined();
16
- expect(setup.headers["x-api-key"]).toBe("real-key");
17
- });
18
-
19
- test("still injects Bearer for a real key", () => {
20
- const setup = resolveOpenAIRequestSetup(
21
- { provider: "custom", id: "m", baseUrl: "http://localhost:8788" },
22
- { apiKey: "sk-real", messages: [] },
23
- );
24
- expect(setup.headers.Authorization).toBe("Bearer sk-real");
25
- });
26
-
27
- test("caller-supplied Authorization in model.headers is preserved even when keyless", () => {
28
- const setup = resolveOpenAIRequestSetup(
29
- {
30
- provider: "qwen",
31
- id: "m",
32
- baseUrl: "http://localhost:8788",
33
- headers: { Authorization: "Bearer custom-token" },
34
- },
35
- { apiKey: NO_AUTH_SENTINEL, messages: [] },
36
- );
37
- expect(setup.headers.Authorization).toBe("Bearer custom-token");
38
- });
39
- });