@oh-my-pi/pi-ai 16.1.12 → 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.
Files changed (98) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/types/providers/aws-credentials.d.ts +2 -0
  3. package/dist/types/providers/devin.d.ts +12 -0
  4. package/dist/types/providers/register-builtins.d.ts +1 -0
  5. package/dist/types/registry/devin.d.ts +8 -0
  6. package/dist/types/registry/huggingface.d.ts +2 -2
  7. package/dist/types/registry/oauth/devin.d.ts +5 -0
  8. package/dist/types/registry/oauth/minimax-code.d.ts +2 -3
  9. package/dist/types/registry/qianfan.d.ts +2 -2
  10. package/dist/types/registry/registry.d.ts +10 -0
  11. package/dist/types/registry/sakana.d.ts +7 -0
  12. package/dist/types/registry/venice.d.ts +2 -2
  13. package/dist/types/registry/zai.d.ts +2 -2
  14. package/dist/types/registry/zhipu-coding-plan.d.ts +2 -2
  15. package/dist/types/types.d.ts +4 -2
  16. package/dist/types/utils/deterministic-id.d.ts +16 -0
  17. package/dist/types/utils/proxy.d.ts +29 -0
  18. package/package.json +5 -4
  19. package/src/api-registry.ts +12 -2
  20. package/src/auth-gateway/server.ts +4 -5
  21. package/src/providers/amazon-bedrock.ts +1 -0
  22. package/src/providers/anthropic-messages-server.ts +4 -1
  23. package/src/providers/aws-credentials.ts +23 -10
  24. package/src/providers/cursor.ts +12 -15
  25. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  26. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  27. package/src/providers/devin/proto/buf.yaml +17 -0
  28. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  29. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  30. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  31. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  32. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  33. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  34. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  35. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  36. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  37. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  38. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  39. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  40. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  41. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  42. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  43. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  44. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  45. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  46. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  47. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  48. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  49. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  50. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  51. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  52. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  53. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  54. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  55. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  56. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  57. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  58. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  59. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  60. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  61. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  62. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  63. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  64. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  65. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  66. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  67. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  68. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  69. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  70. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  71. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  72. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  73. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  74. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  75. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  76. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  77. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  78. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  79. package/src/providers/devin.ts +561 -0
  80. package/src/providers/openai-responses.ts +2 -1
  81. package/src/providers/openai-shared.ts +17 -0
  82. package/src/providers/register-builtins.ts +15 -0
  83. package/src/registry/devin.ts +15 -0
  84. package/src/registry/fireworks.ts +7 -1
  85. package/src/registry/huggingface.ts +13 -35
  86. package/src/registry/oauth/devin.ts +113 -0
  87. package/src/registry/oauth/minimax-code.ts +19 -48
  88. package/src/registry/qianfan.ts +12 -34
  89. package/src/registry/registry.ts +4 -0
  90. package/src/registry/sakana.ts +22 -0
  91. package/src/registry/venice.ts +12 -34
  92. package/src/registry/zai.ts +12 -33
  93. package/src/registry/zhipu-coding-plan.ts +12 -33
  94. package/src/stream.ts +65 -19
  95. package/src/types.ts +4 -2
  96. package/src/utils/deterministic-id.ts +20 -0
  97. package/src/utils/proxy.ts +239 -0
  98. package/src/utils.ts +5 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.14] - 2026-06-22
6
+
7
+ ### Added
8
+
9
+ - Added proxy support for model providers via `PI_PROXY` and `PI_PROXY_<PROVIDER>` variables
10
+ - Added `NO_PROXY` environment variable support for bypassing proxy configuration
11
+ - Added support for Sakana AI provider
12
+ - Added Sakana AI login and request base URL support for `SAKANA_*` / `FUGU_*` environment variables
13
+
14
+ ### Changed
15
+
16
+ - Consolidated API key authentication logic across registry providers
17
+ - Disabled parallel tool calls for Devin provider requests
18
+
19
+ ### Fixed
20
+
21
+ - Improved proxy bypass logic to correctly handle private IP ranges and local metadata services
22
+ - Enhanced memoization for proxy environment variable lookups to improve performance
23
+
24
+ ## [16.1.13] - 2026-06-22
25
+
26
+ ### Added
27
+
28
+ - Added support for Devin as a provider
29
+
30
+ ### Changed
31
+
32
+ - Updated tool call arguments to use `Record<string, unknown>` and `unknown` for tool results
33
+
34
+ ### Fixed
35
+
36
+ - Fixed OpenAI Responses native history replay dropping failed/incomplete image generation calls instead of resending their transient `ig_...` item IDs, preventing follow-up requests from failing with `404 Item with id ... not found`. ([#3225](https://github.com/can1357/oh-my-pi/issues/3225))
37
+ - Fixed `/login fireworks` rejecting valid `fw_…` keys with `Fireworks API key validation failed (500): Error listing deployed models`. The validator pinged `/inference/v1/models`, which Fireworks serves from the per-account deployment registry and 500s for accounts without active deployments. Login now hits the static control-plane `List Models` catalog (`GET /v1/accounts/fireworks/models?filter=supports_serverless=true&pageSize=1`) — the same endpoint discovery already uses — so authentication no longer depends on the caller's deployment state. ([#3219](https://github.com/can1357/oh-my-pi/issues/3219))
38
+
5
39
  ## [16.1.11] - 2026-06-21
6
40
 
7
41
  ### Fixed
@@ -18,6 +18,7 @@
18
18
  * Resolved credentials are cached process-wide per profile and refreshed
19
19
  * 60 s before `Expiration` to absorb clock skew.
20
20
  */
21
+ import type { FetchImpl } from "../types";
21
22
  import type { AwsCredentials } from "./aws-sigv4";
22
23
  export interface ResolvedCredentials extends AwsCredentials {
23
24
  /** Absolute expiration timestamp in ms. `undefined` for non-expiring static creds. */
@@ -29,6 +30,7 @@ export interface CredentialResolveOptions {
29
30
  /** Falls back to env (`AWS_REGION` / `AWS_DEFAULT_REGION`) and finally `us-east-1`. */
30
31
  region?: string;
31
32
  signal?: AbortSignal;
33
+ fetch?: FetchImpl;
32
34
  }
33
35
  export declare function resolveAwsCredentials(opts?: CredentialResolveOptions): Promise<ResolvedCredentials>;
34
36
  /** POSIX-shell-style tokenizer used by the AWS CLI for `credential_process`.
@@ -0,0 +1,12 @@
1
+ import type { StreamFunction, StreamOptions } from "../types";
2
+ /** Base host for Codeium/Windsurf's Cascade chat API (Connect protocol over HTTP/1.1). */
3
+ export declare const DEVIN_API_URL = "https://server.codeium.com";
4
+ export interface DevinOptions extends StreamOptions {
5
+ /** Cascade conversation id; reused as `cascade_id` so the server threads turns. */
6
+ conversationId?: string;
7
+ /** Falls back to `cascade_id` when no `conversationId` is supplied. */
8
+ sessionId?: string;
9
+ /** Wire model uid selected after thinking-effort routing. */
10
+ chatModelUid?: string;
11
+ }
12
+ export declare const streamDevin: StreamFunction<"devin-agent">;
@@ -26,6 +26,7 @@ export declare const streamOpenAICodexResponses: (model: Model<"openai-codex-res
26
26
  export declare const streamOpenAICompletions: (model: Model<"openai-completions">, context: Context, options: OptionsForApi<"openai-completions">) => EventStreamImpl;
27
27
  export declare const streamOpenAIResponses: (model: Model<"openai-responses">, context: Context, options: OptionsForApi<"openai-responses">) => EventStreamImpl;
28
28
  export declare const streamCursor: (model: Model<"cursor-agent">, context: Context, options: OptionsForApi<"cursor-agent">) => EventStreamImpl;
29
+ export declare const streamDevin: (model: Model<"devin-agent">, context: Context, options: OptionsForApi<"devin-agent">) => EventStreamImpl;
29
30
  export declare const streamOllama: (model: Model<"ollama-chat">, context: Context, options: OptionsForApi<"ollama-chat">) => EventStreamImpl;
30
31
  export declare const streamBedrock: (model: Model<"bedrock-converse-stream">, context: Context, options: OptionsForApi<"bedrock-converse-stream">) => EventStreamImpl;
31
32
  export {};
@@ -0,0 +1,8 @@
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const devinProvider: {
3
+ readonly id: "devin";
4
+ readonly name: "Devin";
5
+ readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
6
+ readonly callbackPort: 59653;
7
+ readonly pasteCodeFlow: true;
8
+ };
@@ -1,5 +1,5 @@
1
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
- export declare function loginHuggingface(options: OAuthController): Promise<string>;
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const loginHuggingface: (options: import("./oauth").OAuthController) => Promise<string>;
3
3
  export declare const huggingfaceProvider: {
4
4
  readonly id: "huggingface";
5
5
  readonly name: "Hugging Face Inference";
@@ -0,0 +1,5 @@
1
+ import type { OAuthController, OAuthCredentials } from "./types";
2
+ type FetchFunction = NonNullable<OAuthController["fetch"]>;
3
+ export declare function loginDevin(ctrl: OAuthController): Promise<OAuthCredentials>;
4
+ export declare function exchangeDevinCliToken(authorizationCode: string, codeVerifier: string, fetchImpl?: FetchFunction): Promise<string>;
5
+ export {};
@@ -12,17 +12,16 @@
12
12
  * International: https://api.minimax.io/v1
13
13
  * China: https://api.minimaxi.com/v1
14
14
  */
15
- import type { OAuthController } from "./types";
16
15
  /**
17
16
  * Login to MiniMax Token Plan (international).
18
17
  *
19
18
  * Opens browser to subscription page, prompts user to paste their API key.
20
19
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
21
20
  */
22
- export declare function loginMiniMaxCode(options: OAuthController): Promise<string>;
21
+ export declare const loginMiniMaxCode: (options: import("./types").OAuthController) => Promise<string>;
23
22
  /**
24
23
  * Login to MiniMax Token Plan (China).
25
24
  *
26
25
  * Same flow as international but uses China endpoint.
27
26
  */
28
- export declare function loginMiniMaxCodeCn(options: OAuthController): Promise<string>;
27
+ export declare const loginMiniMaxCodeCn: (options: import("./types").OAuthController) => Promise<string>;
@@ -1,5 +1,5 @@
1
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
- export declare function loginQianfan(options: OAuthController): Promise<string>;
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const loginQianfan: (options: import("./oauth").OAuthController) => Promise<string>;
3
3
  export declare const qianfanProvider: {
4
4
  readonly id: "qianfan";
5
5
  readonly name: "Qianfan";
@@ -46,6 +46,12 @@ declare const ALL: ({
46
46
  readonly id: "deepseek";
47
47
  readonly name: "DeepSeek";
48
48
  readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
49
+ } | {
50
+ readonly id: "devin";
51
+ readonly name: "Devin";
52
+ readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
53
+ readonly callbackPort: 59653;
54
+ readonly pasteCodeFlow: true;
49
55
  } | {
50
56
  readonly id: "firepass";
51
57
  readonly name: "Fire Pass (Fireworks Kimi K2.6 Turbo subscription)";
@@ -202,6 +208,10 @@ declare const ALL: ({
202
208
  readonly id: "qwen-portal";
203
209
  readonly name: "Qwen Portal";
204
210
  readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
211
+ } | {
212
+ readonly id: "sakana";
213
+ readonly name: "Sakana AI";
214
+ readonly login: (cb: import("./oauth").OAuthLoginCallbacks) => Promise<string>;
205
215
  } | {
206
216
  readonly id: "synthetic";
207
217
  readonly name: "Synthetic";
@@ -0,0 +1,7 @@
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const loginSakana: (options: import("./oauth").OAuthController) => Promise<string>;
3
+ export declare const sakanaProvider: {
4
+ readonly id: "sakana";
5
+ readonly name: "Sakana AI";
6
+ readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
7
+ };
@@ -1,11 +1,11 @@
1
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
2
  /**
3
3
  * Login to Venice.
4
4
  *
5
5
  * Opens browser to API keys page, prompts user to paste their API key.
6
6
  * Returns the API key directly (not OAuthCredentials - this isn't OAuth).
7
7
  */
8
- export declare function loginVenice(options: OAuthController): Promise<string>;
8
+ export declare const loginVenice: (options: import("./oauth").OAuthController) => Promise<string>;
9
9
  export declare const veniceProvider: {
10
10
  readonly id: "venice";
11
11
  readonly name: "Venice";
@@ -1,5 +1,5 @@
1
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
- export declare function loginZai(options: OAuthController): Promise<string>;
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const loginZai: (options: import("./oauth").OAuthController) => Promise<string>;
3
3
  export declare const zaiProvider: {
4
4
  readonly id: "zai";
5
5
  readonly name: "Z.AI (GLM Coding Plan)";
@@ -1,5 +1,5 @@
1
- import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
2
- export declare function loginZhipuCodingPlan(options: OAuthController): Promise<string>;
1
+ import type { OAuthLoginCallbacks } from "./oauth/types";
2
+ export declare const loginZhipuCodingPlan: (options: import("./oauth").OAuthController) => Promise<string>;
3
3
  export declare const zhipuCodingPlanProvider: {
4
4
  readonly id: "zhipu-coding-plan";
5
5
  readonly name: "Zhipu Coding Plan (智谱)";
@@ -11,6 +11,7 @@ import type { AnthropicOptions } from "./providers/anthropic";
11
11
  import type { StopDetails } from "./providers/anthropic-wire";
12
12
  import type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses";
13
13
  import type { CursorOptions } from "./providers/cursor";
14
+ import type { DevinOptions } from "./providers/devin";
14
15
  import type { GoogleOptions } from "./providers/google";
15
16
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
16
17
  import type { GoogleVertexOptions } from "./providers/google-vertex";
@@ -46,6 +47,7 @@ export interface ApiOptionsMap {
46
47
  "google-vertex": GoogleVertexOptions;
47
48
  "ollama-chat": OllamaChatOptions;
48
49
  "cursor-agent": CursorOptions;
50
+ "devin-agent": DevinOptions;
49
51
  }
50
52
  export type OptionsForApi<TApi extends Api> = StreamOptions | (TApi extends keyof ApiOptionsMap ? ApiOptionsMap[TApi] : never);
51
53
  export interface TokenTaskBudget {
@@ -354,7 +356,7 @@ export interface ToolCall {
354
356
  type: "toolCall";
355
357
  id: string;
356
358
  name: string;
357
- arguments: Record<string, any>;
359
+ arguments: Record<string, unknown>;
358
360
  thoughtSignature?: string;
359
361
  intent?: string;
360
362
  /**
@@ -442,7 +444,7 @@ export interface AssistantMessage {
442
444
  duration?: number;
443
445
  ttft?: number;
444
446
  }
445
- export interface ToolResultMessage<TDetails = any> {
447
+ export interface ToolResultMessage<TDetails = unknown> {
446
448
  role: "toolResult";
447
449
  toolCallId: string;
448
450
  toolName: string;
@@ -0,0 +1,16 @@
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
+ * Format the leading 128 bits of `seed`'s SHA-256 digest as a v4-shape UUID
10
+ * (8-4-4-4-12 hex groups).
11
+ *
12
+ * Deterministic: identical seeds always map to the same id, so callers get
13
+ * stable ids across requests / conversation turns (reusing message-blob ids,
14
+ * keying prompt caches) without persisting a seed→id mapping.
15
+ */
16
+ export declare function deterministicUuid(seed: string): DeterministicUuid;
@@ -0,0 +1,29 @@
1
+ import * as tls from "node:tls";
2
+ import type { FetchImpl } from "../types";
3
+ /**
4
+ * Checks if a host is local or cloud metadata, which should always bypass the proxy
5
+ * (e.g. localhost, 127/8, ::1, 169.254.169.254, metadata.google.internal).
6
+ */
7
+ export declare function isLocalOrMetadataHost(host: string): boolean;
8
+ /**
9
+ * Check if the url should bypass the proxy due to hard-coded localhost/metadata checks
10
+ * or custom NO_PROXY/no_proxy environment variables rules.
11
+ */
12
+ export declare function shouldBypassProxy(urlObj: URL): boolean;
13
+ /** Test seam: clears the provider proxy cache. */
14
+ export declare function __resetProxyCache(): void;
15
+ /**
16
+ * Normalizes provider id (e.g. github-copilot -> PI_PROXY_GITHUB_COPILOT) and looks it up.
17
+ * If not found, falls back to PI_PROXY. Results are memoized because env values are static
18
+ * for the lifetime of the process and this function is called for every outgoing request.
19
+ */
20
+ export declare function getProxyForProvider(provider: string): string | undefined;
21
+ /**
22
+ * Wraps a fetch implementation to inject proxy options for non-local hosts.
23
+ */
24
+ export declare function wrapFetchForProxy(fetchImpl: FetchImpl, provider: string): FetchImpl;
25
+ /**
26
+ * Tunnel a socket connection through an HTTP CONNECT proxy.
27
+ * This is used specifically to wrap Node's `http2.connect(baseUrl, { createConnection })` for Cursor.
28
+ */
29
+ export declare function connectProxiedSocket(proxyUrlStr: string, targetUrlStr: string): Promise<tls.TLSSocket>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.1.12",
4
+ "version": "16.1.14",
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,13 +38,14 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "16.1.12",
42
- "@oh-my-pi/pi-utils": "16.1.12",
43
- "@oh-my-pi/pi-wire": "16.1.12",
41
+ "@oh-my-pi/pi-catalog": "16.1.14",
42
+ "@oh-my-pi/pi-utils": "16.1.14",
43
+ "@oh-my-pi/pi-wire": "16.1.14",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
47
47
  "devDependencies": {
48
+ "@bufbuild/protoc-gen-es": "^2.12.0",
48
49
  "@types/bun": "^1.3.14"
49
50
  },
50
51
  "engines": {
@@ -14,9 +14,10 @@ import type {
14
14
  StreamOptions,
15
15
  } from "./types";
16
16
 
17
- const BUILTIN_APIS = new Set<KnownApi>([
17
+ const BUILTIN_API_IDS = [
18
18
  "openai-completions",
19
19
  "openai-responses",
20
+ "openrouter",
20
21
  "openai-codex-responses",
21
22
  "azure-openai-responses",
22
23
  "anthropic-messages",
@@ -26,7 +27,16 @@ const BUILTIN_APIS = new Set<KnownApi>([
26
27
  "google-vertex",
27
28
  "ollama-chat",
28
29
  "cursor-agent",
29
- ]);
30
+ "devin-agent",
31
+ ] as const satisfies readonly KnownApi[];
32
+
33
+ type _MissingBuiltinApis = Exclude<KnownApi, (typeof BUILTIN_API_IDS)[number]>;
34
+ type _CheckBuiltinApis = _MissingBuiltinApis extends never
35
+ ? true
36
+ : ["BUILTIN_APIS is missing KnownApi values", _MissingBuiltinApis];
37
+ true satisfies _CheckBuiltinApis;
38
+
39
+ const BUILTIN_APIS = new Set<KnownApi>(BUILTIN_API_IDS);
30
40
 
31
41
  export type CustomStreamFn = (
32
42
  model: Model<Api>,
@@ -29,6 +29,7 @@ import * as piNative from "../providers/pi-native-server";
29
29
  import { isUsageLimitError } from "../rate-limit-utils";
30
30
  import { streamSimple } from "../stream";
31
31
  import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "../types";
32
+ import { deterministicUuid } from "../utils/deterministic-id";
32
33
  import { parseBind } from "../utils/parse-bind";
33
34
  import { captureRequestHeaders, corsHeaders, isAuthorized, json, resolvePeer, withCors } from "./http";
34
35
  import type {
@@ -110,11 +111,9 @@ function deriveSessionId(modelId: string, context: Context): string {
110
111
  parts.push(JSON.stringify({ role: first.role, content: first.content }));
111
112
  }
112
113
  const seed = parts.join("\u0000");
113
- const hex = new Bun.CryptoHasher("sha256").update(seed).digest("hex");
114
- // Format the leading 128 bits as a v4-shape UUID (8-4-4-4-12). Codex's
115
- // `normalizeOpenAIResponsesPromptCacheKey` accepts ≤64 chars verbatim, so
116
- // the 36-char UUID flows through unchanged.
117
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
114
+ // The 36-char UUID flows through unchanged: Codex's
115
+ // `normalizeOpenAIResponsesPromptCacheKey` accepts ≤64 chars verbatim.
116
+ return deterministicUuid(seed);
118
117
  }
119
118
 
120
119
  function buildStreamOptions(parsed: ParsedFormatRequest, api: Api, signal: AbortSignal): SimpleStreamOptions {
@@ -302,6 +302,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
302
302
  profile: options.profile,
303
303
  region,
304
304
  signal: options.signal,
305
+ fetch: options.fetch,
305
306
  });
306
307
  }
307
308
  const signed = await signRequest({
@@ -204,7 +204,10 @@ function walkAssistantContent(
204
204
  type: "toolCall",
205
205
  id: block.id,
206
206
  name: block.name,
207
- arguments: block.input ?? {},
207
+ arguments:
208
+ block.input && typeof block.input === "object" && !Array.isArray(block.input)
209
+ ? (block.input as Record<string, unknown>)
210
+ : {},
208
211
  });
209
212
  break;
210
213
  default: {
@@ -23,6 +23,7 @@ import * as fs from "node:fs";
23
23
  import * as os from "node:os";
24
24
  import * as path from "node:path";
25
25
  import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
26
+ import type { FetchImpl } from "../types";
26
27
  import { raceWithSignal } from "../utils/abort";
27
28
  import type { AwsCredentials } from "./aws-sigv4";
28
29
 
@@ -37,6 +38,7 @@ export interface CredentialResolveOptions {
37
38
  /** Falls back to env (`AWS_REGION` / `AWS_DEFAULT_REGION`) and finally `us-east-1`. */
38
39
  region?: string;
39
40
  signal?: AbortSignal;
41
+ fetch?: FetchImpl;
40
42
  }
41
43
 
42
44
  const REFRESH_SKEW_MS = 60_000;
@@ -75,9 +77,10 @@ export async function resolveAwsCredentials(opts: CredentialResolveOptions = {})
75
77
  const existing = inflight.get(cacheKey);
76
78
  if (existing) return raceWithSignal(existing, opts.signal);
77
79
 
80
+ const fetchImpl = opts.fetch ?? (globalThis.fetch as FetchImpl);
78
81
  const promise = (async () => {
79
82
  try {
80
- const creds = await resolveFresh(profile, region, AbortSignal.timeout(SHARED_RESOLVE_TIMEOUT_MS));
83
+ const creds = await resolveFresh(profile, region, AbortSignal.timeout(SHARED_RESOLVE_TIMEOUT_MS), fetchImpl);
81
84
  cache.set(cacheKey, { creds, expiresAt: creds.expiresAt ?? Number.POSITIVE_INFINITY });
82
85
  return creds;
83
86
  } finally {
@@ -88,18 +91,23 @@ export async function resolveAwsCredentials(opts: CredentialResolveOptions = {})
88
91
  return raceWithSignal(promise, opts.signal);
89
92
  }
90
93
 
91
- async function resolveFresh(profile: string, region: string, signal?: AbortSignal): Promise<ResolvedCredentials> {
94
+ async function resolveFresh(
95
+ profile: string,
96
+ region: string,
97
+ signal?: AbortSignal,
98
+ fetchImpl: FetchImpl = globalThis.fetch as FetchImpl,
99
+ ): Promise<ResolvedCredentials> {
92
100
  // 1. Environment first — matches the AWS SDK chain order.
93
101
  const envCreds = readEnvCredentials();
94
102
  if (envCreds) return envCreds;
95
103
 
96
104
  // 2. Profile (static or SSO).
97
- const profileCreds = await readProfileCredentials(profile, region, signal);
105
+ const profileCreds = await readProfileCredentials(profile, region, signal, fetchImpl);
98
106
  if (profileCreds) return profileCreds;
99
107
 
100
108
  // 3. EC2 IMDSv2.
101
109
  if ($env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true") {
102
- const imdsCreds = await readImdsCredentials(signal);
110
+ const imdsCreds = await readImdsCredentials(signal, fetchImpl);
103
111
  if (imdsCreds) return imdsCreds;
104
112
  }
105
113
 
@@ -167,6 +175,7 @@ async function readProfileCredentials(
167
175
  profile: string,
168
176
  region: string,
169
177
  signal: AbortSignal | undefined,
178
+ fetchImpl: FetchImpl,
170
179
  ): Promise<ResolvedCredentials | undefined> {
171
180
  const home = os.homedir();
172
181
  const credentialsPath = $env.AWS_SHARED_CREDENTIALS_FILE || path.join(home, ".aws", "credentials");
@@ -195,7 +204,7 @@ async function readProfileCredentials(
195
204
  }
196
205
 
197
206
  if (merged.sso_account_id && merged.sso_role_name) {
198
- return readSsoCredentials(merged, configIni, region, signal);
207
+ return readSsoCredentials(merged, configIni, region, signal, fetchImpl);
199
208
  }
200
209
 
201
210
  if (merged.credential_process) {
@@ -217,6 +226,7 @@ async function readSsoCredentials(
217
226
  configIni: IniFile | undefined,
218
227
  defaultRegion: string,
219
228
  signal: AbortSignal | undefined,
229
+ fetchImpl: FetchImpl,
220
230
  ): Promise<ResolvedCredentials | undefined> {
221
231
  // Two SSO profile shapes:
222
232
  // - legacy: `sso_start_url` + `sso_region` directly on the profile
@@ -246,7 +256,7 @@ async function readSsoCredentials(
246
256
  `https://portal.sso.${ssoRegion}.amazonaws.com/federation/credentials` +
247
257
  `?account_id=${encodeURIComponent(profileCfg.sso_account_id)}` +
248
258
  `&role_name=${encodeURIComponent(profileCfg.sso_role_name)}`;
249
- const response = await fetch(url, {
259
+ const response = await fetchImpl(url, {
250
260
  method: "GET",
251
261
  headers: { "x-amz-sso_bearer_token": token.accessToken },
252
262
  signal,
@@ -480,11 +490,14 @@ export function tokenizeCredentialProcessCommand(cmd: string): string[] {
480
490
  const IMDS_HOST = "169.254.169.254";
481
491
  const IMDS_TIMEOUT_MS = 1000;
482
492
 
483
- async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promise<ResolvedCredentials | undefined> {
493
+ async function readImdsCredentials(
494
+ parentSignal: AbortSignal | undefined,
495
+ fetchImpl: FetchImpl,
496
+ ): Promise<ResolvedCredentials | undefined> {
484
497
  const timeout = AbortSignal.timeout(IMDS_TIMEOUT_MS);
485
498
  const signal = parentSignal ? AbortSignal.any([parentSignal, timeout]) : timeout;
486
499
  try {
487
- const tokenRes = await fetch(`http://${IMDS_HOST}/latest/api/token`, {
500
+ const tokenRes = await fetchImpl(`http://${IMDS_HOST}/latest/api/token`, {
488
501
  method: "PUT",
489
502
  headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
490
503
  signal,
@@ -492,7 +505,7 @@ async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promi
492
505
  if (!tokenRes.ok) return undefined;
493
506
  const token = await tokenRes.text();
494
507
 
495
- const roleRes = await fetch(`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/`, {
508
+ const roleRes = await fetchImpl(`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/`, {
496
509
  headers: { "x-aws-ec2-metadata-token": token },
497
510
  signal,
498
511
  });
@@ -500,7 +513,7 @@ async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promi
500
513
  const role = (await roleRes.text()).trim();
501
514
  if (!role) return undefined;
502
515
 
503
- const credsRes = await fetch(
516
+ const credsRes = await fetchImpl(
504
517
  `http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
505
518
  {
506
519
  headers: { "x-aws-ec2-metadata-token": token },
@@ -124,8 +124,10 @@ import type {
124
124
  ToolResultMessage,
125
125
  } from "../types";
126
126
  import { normalizeSystemPrompts } from "../utils";
127
+ import { deterministicUuid } from "../utils/deterministic-id";
127
128
  import { AssistantMessageEventStream } from "../utils/event-stream";
128
129
  import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
130
+ import { connectProxiedSocket, getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
129
131
  import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
130
132
  import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
131
133
  import { toolWireSchema } from "../utils/schema/wire";
@@ -376,7 +378,15 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
376
378
  })
377
379
  : undefined;
378
380
 
379
- h2Client = http2.connect(baseUrl);
381
+ const proxyUrl = shouldBypassProxy(new URL(baseUrl)) ? undefined : getProxyForProvider(model.provider);
382
+ if (proxyUrl) {
383
+ const tlsSocket = await connectProxiedSocket(proxyUrl, baseUrl);
384
+ h2Client = http2.connect(baseUrl, {
385
+ createConnection: () => tlsSocket,
386
+ });
387
+ } else {
388
+ h2Client = http2.connect(baseUrl);
389
+ }
380
390
 
381
391
  h2Request = h2Client.request(requestHeaders);
382
392
 
@@ -2258,19 +2268,6 @@ function extractAssistantMessageText(msg: Message): string {
2258
2268
  .join("\n");
2259
2269
  }
2260
2270
 
2261
- /**
2262
- * Derive a stable, UUID-formatted `message_id` from a content key.
2263
- * Ensures identical historical messages hash to the same blob IDs across
2264
- * requests, so `conversationBlobStores` does not grow unboundedly and
2265
- * unchanged history reuses existing blob IDs.
2266
- */
2267
- type CursorMessageId = `${string}-${string}-${string}-${string}-${string}`;
2268
-
2269
- function deterministicMessageId(key: string): CursorMessageId {
2270
- const hex = createHash("sha256").update(key).digest("hex");
2271
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
2272
- }
2273
-
2274
2271
  /**
2275
2272
  * Index of the last user/developer message in `messages`, or -1 if none.
2276
2273
  * Used to exclude the current user turn from history builders — it goes in
@@ -2394,7 +2391,7 @@ function buildConversationTurns(
2394
2391
  const userMessage = createCursorUserMessage(
2395
2392
  msg.content,
2396
2393
  userText,
2397
- deterministicMessageId(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
2394
+ deterministicUuid(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
2398
2395
  );
2399
2396
  const userMessageBytes = toBinary(UserMessageSchema, userMessage);
2400
2397
  const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);