@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.
- package/CHANGELOG.md +19 -0
- package/dist/types/providers/aws-credentials.d.ts +2 -0
- package/dist/types/registry/huggingface.d.ts +2 -2
- package/dist/types/registry/oauth/minimax-code.d.ts +2 -3
- package/dist/types/registry/qianfan.d.ts +2 -2
- package/dist/types/registry/registry.d.ts +4 -0
- package/dist/types/registry/sakana.d.ts +7 -0
- package/dist/types/registry/venice.d.ts +2 -2
- package/dist/types/registry/zai.d.ts +2 -2
- package/dist/types/registry/zhipu-coding-plan.d.ts +2 -2
- package/dist/types/utils/deterministic-id.d.ts +16 -0
- package/dist/types/utils/proxy.d.ts +29 -0
- package/package.json +4 -4
- package/src/auth-gateway/server.ts +4 -5
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/aws-credentials.ts +23 -10
- package/src/providers/cursor.ts +12 -15
- package/src/providers/devin.ts +7 -14
- package/src/providers/openai-responses.ts +2 -1
- package/src/providers/openai-shared.ts +17 -0
- package/src/registry/huggingface.ts +13 -35
- package/src/registry/oauth/minimax-code.ts +19 -48
- package/src/registry/qianfan.ts +12 -34
- package/src/registry/registry.ts +2 -0
- package/src/registry/sakana.ts +22 -0
- package/src/registry/venice.ts +12 -34
- package/src/registry/zai.ts +12 -33
- package/src/registry/zhipu-coding-plan.ts +12 -33
- package/src/stream.ts +18 -13
- package/src/utils/deterministic-id.ts +20 -0
- package/src/utils/proxy.ts +239 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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
|
+
|
|
5
24
|
## [16.1.13] - 2026-06-22
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -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`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
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";
|
|
@@ -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
|
|
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
|
|
27
|
+
export declare const loginMiniMaxCodeCn: (options: import("./types").OAuthController) => Promise<string>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
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";
|
|
@@ -208,6 +208,10 @@ declare const ALL: ({
|
|
|
208
208
|
readonly id: "qwen-portal";
|
|
209
209
|
readonly name: "Qwen Portal";
|
|
210
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>;
|
|
211
215
|
} | {
|
|
212
216
|
readonly id: "synthetic";
|
|
213
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 {
|
|
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
|
|
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 {
|
|
2
|
-
export declare
|
|
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 {
|
|
2
|
-
export declare
|
|
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 (智谱)";
|
|
@@ -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.
|
|
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,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
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
|
},
|
|
@@ -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
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
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 {
|
|
@@ -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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 },
|
package/src/providers/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2394
|
+
deterministicUuid(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
|
|
2398
2395
|
);
|
|
2399
2396
|
const userMessageBytes = toBinary(UserMessageSchema, userMessage);
|
|
2400
2397
|
const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);
|
package/src/providers/devin.ts
CHANGED
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
Tool,
|
|
43
43
|
ToolCall,
|
|
44
44
|
} from "../types";
|
|
45
|
+
import { deterministicUuid } from "../utils/deterministic-id";
|
|
45
46
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
46
47
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
47
48
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
@@ -427,7 +428,7 @@ function buildDevinChatRequest(
|
|
|
427
428
|
plannerMode: ConversationalPlannerMode.DEFAULT,
|
|
428
429
|
toolChoice: create(ChatToolChoiceSchema, { choice: { case: "optionName", value: "auto" } }),
|
|
429
430
|
systemPromptCacheOptions: create(PromptCacheOptionsSchema, { type: CacheControlType.EPHEMERAL }),
|
|
430
|
-
disableParallelToolCalls:
|
|
431
|
+
disableParallelToolCalls: true,
|
|
431
432
|
cascadeId,
|
|
432
433
|
executionId: crypto.randomUUID(),
|
|
433
434
|
configuration: create(CompletionConfigurationSchema, {
|
|
@@ -455,6 +456,8 @@ function buildDevinChatRequest(
|
|
|
455
456
|
/** Map omp `Message` history onto Cascade `ChatMessagePrompt`s (USER / SYSTEM / TOOL channels). */
|
|
456
457
|
function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMessagePrompt[] {
|
|
457
458
|
const prompts: ChatMessagePrompt[] = [];
|
|
459
|
+
// messageId seeds are `cascadeId\0index\0role[...]` — prompt text is excluded
|
|
460
|
+
// so ids stay stable across content edits / history rebuilds.
|
|
458
461
|
for (const [index, msg] of messages.entries()) {
|
|
459
462
|
if (msg.role === "user" || msg.role === "developer") {
|
|
460
463
|
let promptText = "";
|
|
@@ -472,7 +475,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
472
475
|
}
|
|
473
476
|
prompts.push(
|
|
474
477
|
create(ChatMessagePromptSchema, {
|
|
475
|
-
messageId:
|
|
478
|
+
messageId: deterministicUuid(`${cascadeId}\0${index}\0${msg.role}`),
|
|
476
479
|
source: ChatMessageSource.USER,
|
|
477
480
|
prompt: promptText,
|
|
478
481
|
images,
|
|
@@ -501,7 +504,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
501
504
|
}
|
|
502
505
|
prompts.push(
|
|
503
506
|
create(ChatMessagePromptSchema, {
|
|
504
|
-
messageId: msg.responseId ?? `bot-${
|
|
507
|
+
messageId: msg.responseId ?? `bot-${deterministicUuid(`${cascadeId}\0${index}\0assistant`)}`,
|
|
505
508
|
source: ChatMessageSource.SYSTEM,
|
|
506
509
|
prompt: promptText,
|
|
507
510
|
thinking: thinkingText,
|
|
@@ -522,7 +525,7 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
522
525
|
}
|
|
523
526
|
prompts.push(
|
|
524
527
|
create(ChatMessagePromptSchema, {
|
|
525
|
-
messageId:
|
|
528
|
+
messageId: deterministicUuid(`${cascadeId}\0${index}\0tool\0${msg.toolCallId}`),
|
|
526
529
|
source: ChatMessageSource.TOOL,
|
|
527
530
|
toolCallId: msg.toolCallId,
|
|
528
531
|
toolResultIsError: msg.isError,
|
|
@@ -535,16 +538,6 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
535
538
|
return prompts;
|
|
536
539
|
}
|
|
537
540
|
|
|
538
|
-
/**
|
|
539
|
-
* Deterministic UUID-shaped message id derived from a stable per-message seed.
|
|
540
|
-
* Seed is `${cascadeId}\0${index}\0${role}[...]` — prompt text is intentionally
|
|
541
|
-
* excluded so ids stay stable across content edits / history rebuilds.
|
|
542
|
-
*/
|
|
543
|
-
function deterministicMessageId(seed: string): string {
|
|
544
|
-
const hex = new Bun.CryptoHasher("sha256").update(seed).digest("hex");
|
|
545
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
541
|
/**
|
|
549
542
|
* Parse a Connect end-of-stream JSON trailer and return a human-readable error
|
|
550
543
|
* string when it carries `{ error: { code, message } }`, else `null`. The trailer
|
|
@@ -436,7 +436,8 @@ const streamOpenAIResponsesOnce = (
|
|
|
436
436
|
? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
|
|
437
437
|
: { params };
|
|
438
438
|
sentPreviousResponseId = chained.previousResponseId;
|
|
439
|
-
const idleTimeoutMs =
|
|
439
|
+
const idleTimeoutMs =
|
|
440
|
+
options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
|
|
440
441
|
const firstEventTimeoutMs =
|
|
441
442
|
options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
442
443
|
const requestTimeoutMs =
|
|
@@ -130,6 +130,17 @@ export interface OpenAIRequestSetup {
|
|
|
130
130
|
requestHeaders: Record<string, string>;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
134
|
+
const value = baseUrl?.trim();
|
|
135
|
+
if (!value) return undefined;
|
|
136
|
+
const normalized = value.replace(/\/+$/, "");
|
|
137
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveSakanaRequestBaseUrl(): string | undefined {
|
|
141
|
+
return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
export function resolveOpenAIRequestSetup(
|
|
134
145
|
model: OpenAIRequestSetupModel,
|
|
135
146
|
options: OpenAIRequestSetupOptions,
|
|
@@ -165,6 +176,12 @@ export function resolveOpenAIRequestSetup(
|
|
|
165
176
|
baseUrl = moonshotBaseUrl;
|
|
166
177
|
}
|
|
167
178
|
}
|
|
179
|
+
if (model.provider === "sakana") {
|
|
180
|
+
const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
|
|
181
|
+
if (sakanaBaseUrl) {
|
|
182
|
+
baseUrl = sakanaBaseUrl;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
168
185
|
if (model.provider === "github-copilot") {
|
|
169
186
|
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
170
187
|
const copilot = buildCopilotDynamicHeaders({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
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 =
|
|
@@ -7,42 +7,20 @@ const AUTH_URL =
|
|
|
7
7
|
const API_BASE_URL = "https://router.huggingface.co/v1";
|
|
8
8
|
const VALIDATION_MODEL = "openai/gpt-oss-120b";
|
|
9
9
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const apiKey = await options.onPrompt({
|
|
22
|
-
message: "Paste your Hugging Face token (HUGGINGFACE_HUB_TOKEN / HF_TOKEN)",
|
|
23
|
-
placeholder: "hf_...",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (options.signal?.aborted) {
|
|
27
|
-
throw new Error("Login cancelled");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const trimmed = apiKey.trim();
|
|
31
|
-
if (!trimmed) {
|
|
32
|
-
throw new Error("API key is required");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
options.onProgress?.("Validating API key...");
|
|
36
|
-
await validateOpenAICompatibleApiKey({
|
|
10
|
+
export const loginHuggingface = createApiKeyLogin({
|
|
11
|
+
providerLabel: "Hugging Face",
|
|
12
|
+
authUrl: AUTH_URL,
|
|
13
|
+
instructions:
|
|
14
|
+
"Create/copy a token with Make calls to Inference Providers permission (usable as HUGGINGFACE_HUB_TOKEN or HF_TOKEN)",
|
|
15
|
+
promptMessage: "Paste your Hugging Face token (HUGGINGFACE_HUB_TOKEN / HF_TOKEN)",
|
|
16
|
+
placeholder: "hf_...",
|
|
17
|
+
validation: {
|
|
18
|
+
kind: "chat-completions",
|
|
37
19
|
provider: "Hugging Face",
|
|
38
|
-
apiKey: trimmed,
|
|
39
20
|
baseUrl: API_BASE_URL,
|
|
40
21
|
model: VALIDATION_MODEL,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return trimmed;
|
|
45
|
-
}
|
|
22
|
+
},
|
|
23
|
+
});
|
|
46
24
|
|
|
47
25
|
export const huggingfaceProvider = {
|
|
48
26
|
id: "huggingface",
|