@oh-my-pi/pi-ai 16.2.1 → 16.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/types/auth-gateway/server.d.ts +0 -19
- package/dist/types/auth-retry.d.ts +2 -10
- package/dist/types/auth-storage.d.ts +1 -2
- package/dist/types/dialect/demotion.d.ts +22 -0
- package/dist/types/dialect/index.d.ts +2 -0
- package/dist/types/error/abort.d.ts +14 -0
- package/dist/types/error/auth-classify.d.ts +16 -0
- package/dist/types/error/auth.d.ts +27 -0
- package/dist/types/error/aws.d.ts +23 -0
- package/dist/types/error/classes.d.ts +102 -0
- package/dist/types/error/finalize.d.ts +39 -0
- package/dist/types/error/flags.d.ts +70 -0
- package/dist/types/error/format.d.ts +20 -0
- package/dist/types/error/gateway.d.ts +20 -0
- package/dist/types/error/index.d.ts +13 -0
- package/dist/types/error/oauth.d.ts +43 -0
- package/dist/types/error/provider.d.ts +42 -0
- package/dist/types/{rate-limit-utils.d.ts → error/rate-limit.d.ts} +14 -1
- package/dist/types/error/retryable.d.ts +27 -0
- package/dist/types/error/validation.d.ts +32 -0
- package/dist/types/index.d.ts +1 -3
- package/dist/types/providers/amazon-bedrock.d.ts +0 -5
- package/dist/types/providers/anthropic-client.d.ts +2 -16
- package/dist/types/providers/anthropic.d.ts +6 -1
- package/dist/types/providers/aws-eventstream.d.ts +2 -1
- package/dist/types/providers/cursor.d.ts +8 -7
- package/dist/types/providers/google-gemini-cli.d.ts +0 -5
- package/dist/types/providers/google-shared.d.ts +0 -5
- package/dist/types/providers/ollama.d.ts +0 -5
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -2
- package/dist/types/providers/openai-codex/response-handler.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -2
- package/dist/types/providers/openai-responses-server-schema.d.ts +6 -0
- package/dist/types/providers/openai-responses-wire.d.ts +1 -1
- package/dist/types/providers/openai-responses.d.ts +1 -0
- package/dist/types/providers/openai-shared.d.ts +8 -6
- package/dist/types/providers/pi-native-client.d.ts +0 -9
- package/dist/types/registry/oauth/xai-oauth.d.ts +0 -9
- package/dist/types/types.d.ts +6 -0
- package/dist/types/utils/block-symbols.d.ts +20 -0
- package/dist/types/utils/openai-http.d.ts +4 -8
- package/dist/types/utils/retry.d.ts +2 -14
- package/dist/types/utils/thinking-loop.d.ts +3 -1
- package/package.json +8 -4
- package/src/api-registry.ts +3 -1
- package/src/auth-broker/discover.ts +7 -2
- package/src/auth-broker/remote-store.ts +8 -7
- package/src/auth-gateway/server.ts +27 -115
- package/src/auth-retry.ts +9 -21
- package/src/auth-storage.ts +34 -49
- package/src/dialect/demotion.ts +31 -0
- package/src/dialect/factory.ts +0 -2
- package/src/dialect/index.ts +2 -0
- package/src/dialect/owned-stream.ts +0 -1
- package/src/dialect/thinking.ts +22 -10
- package/src/error/abort.ts +18 -0
- package/src/error/auth-classify.ts +30 -0
- package/src/error/auth.ts +48 -0
- package/src/error/aws.ts +31 -0
- package/src/error/classes.ts +186 -0
- package/src/error/finalize.ts +69 -0
- package/src/error/flags.ts +486 -0
- package/src/error/format.ts +36 -0
- package/src/error/gateway.ts +96 -0
- package/src/error/index.ts +13 -0
- package/src/error/oauth.ts +58 -0
- package/src/error/provider.ts +56 -0
- package/src/{rate-limit-utils.ts → error/rate-limit.ts} +9 -3
- package/src/error/retryable.ts +70 -0
- package/src/error/validation.ts +44 -0
- package/src/index.ts +1 -3
- package/src/providers/amazon-bedrock.ts +61 -57
- package/src/providers/anthropic-client.ts +13 -41
- package/src/providers/anthropic-messages-server.ts +9 -2
- package/src/providers/anthropic.ts +84 -147
- package/src/providers/aws-credentials.ts +41 -11
- package/src/providers/aws-eventstream.ts +12 -21
- package/src/providers/azure-openai-responses.ts +27 -17
- package/src/providers/cursor.ts +59 -53
- package/src/providers/devin.ts +28 -20
- package/src/providers/gitlab-duo-workflow.ts +30 -10
- package/src/providers/gitlab-duo.ts +22 -6
- package/src/providers/google-auth.ts +15 -5
- package/src/providers/google-gemini-cli.ts +46 -60
- package/src/providers/google-shared.ts +40 -38
- package/src/providers/google-vertex.ts +3 -2
- package/src/providers/google.ts +5 -1
- package/src/providers/mock.ts +10 -7
- package/src/providers/ollama.ts +34 -29
- package/src/providers/openai-chat-server.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +13 -12
- package/src/providers/openai-codex/response-handler.ts +1 -1
- package/src/providers/openai-codex-responses.ts +931 -1012
- package/src/providers/openai-completions.ts +46 -36
- package/src/providers/openai-responses-server-schema.ts +3 -0
- package/src/providers/openai-responses-server.ts +82 -30
- package/src/providers/openai-responses-wire.ts +1 -1
- package/src/providers/openai-responses.ts +46 -22
- package/src/providers/openai-shared.ts +118 -51
- package/src/providers/pi-native-client.ts +12 -18
- package/src/providers/pi-native-server.ts +9 -6
- package/src/providers/register-builtins.ts +5 -2
- package/src/providers/transform-messages.ts +31 -63
- package/src/registry/alibaba-coding-plan.ts +6 -5
- package/src/registry/api-key-login.ts +4 -3
- package/src/registry/api-key-validation.ts +4 -3
- package/src/registry/cloudflare-ai-gateway.ts +4 -3
- package/src/registry/coreweave.ts +2 -1
- package/src/registry/deepseek.ts +2 -1
- package/src/registry/google-antigravity.ts +2 -1
- package/src/registry/google-gemini-cli.ts +2 -1
- package/src/registry/kagi.ts +4 -3
- package/src/registry/kilo.ts +32 -10
- package/src/registry/litellm.ts +4 -3
- package/src/registry/llama-cpp.ts +3 -2
- package/src/registry/lm-studio.ts +3 -2
- package/src/registry/nvidia.ts +7 -7
- package/src/registry/oauth/anthropic.ts +23 -6
- package/src/registry/oauth/callback-server.ts +5 -3
- package/src/registry/oauth/cursor.ts +18 -4
- package/src/registry/oauth/devin.ts +14 -3
- package/src/registry/oauth/github-copilot.ts +21 -10
- package/src/registry/oauth/gitlab-duo-workflow.ts +15 -3
- package/src/registry/oauth/gitlab-duo.ts +30 -6
- package/src/registry/oauth/google-antigravity.ts +14 -5
- package/src/registry/oauth/google-gemini-cli.ts +24 -7
- package/src/registry/oauth/google-oauth-shared.ts +6 -3
- package/src/registry/oauth/index.ts +19 -8
- package/src/registry/oauth/kimi.ts +40 -10
- package/src/registry/oauth/openai-codex.ts +26 -11
- package/src/registry/oauth/opencode.ts +4 -3
- package/src/registry/oauth/perplexity.ts +33 -11
- package/src/registry/oauth/xai-oauth.ts +62 -18
- package/src/registry/oauth/xiaomi.ts +21 -9
- package/src/registry/ollama-cloud.ts +5 -4
- package/src/registry/ollama.ts +3 -2
- package/src/registry/parallel.ts +4 -3
- package/src/registry/qwen-portal.ts +4 -3
- package/src/registry/tavily.ts +4 -3
- package/src/registry/vercel-ai-gateway.ts +4 -3
- package/src/registry/vllm.ts +3 -2
- package/src/stream.ts +79 -24
- package/src/types.ts +6 -0
- package/src/usage/claude.ts +2 -1
- package/src/usage/github-copilot.ts +4 -3
- package/src/usage/google-antigravity.ts +2 -1
- package/src/utils/abort.ts +4 -2
- package/src/utils/block-symbols.ts +32 -0
- package/src/utils/event-stream.ts +15 -3
- package/src/utils/http-inspector.ts +4 -4
- package/src/utils/idle-iterator.ts +6 -5
- package/src/utils/openai-http.ts +11 -46
- package/src/utils/parse-bind.ts +7 -5
- package/src/utils/proxy.ts +2 -1
- package/src/utils/retry.ts +7 -23
- package/src/utils/schema/normalize.ts +3 -2
- package/src/utils/thinking-loop.ts +15 -11
- package/src/utils/validation.ts +4 -3
- package/dist/types/dialect/pi.d.ts +0 -9
- package/dist/types/errors.d.ts +0 -24
- package/dist/types/utils/overflow.d.ts +0 -55
- package/src/dialect/pi.md +0 -55
- package/src/dialect/pi.ts +0 -600
- package/src/errors.ts +0 -32
- package/src/utils/overflow.ts +0 -140
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
discoverGitLabDuoWorkflowRuntimeNamespace,
|
|
5
5
|
type GitLabDuoWorkflowNamespaceSelection,
|
|
6
6
|
} from "@oh-my-pi/pi-catalog/discovery/gitlab-duo-workflow";
|
|
7
|
+
import * as AIError from "../error";
|
|
7
8
|
import type {
|
|
8
9
|
Api,
|
|
9
10
|
AssistantMessage,
|
|
@@ -109,7 +110,7 @@ const GITLAB_DUO_WORKFLOW_GOAL_SOFT_OVERFLOW_BYTES = 1_048_576;
|
|
|
109
110
|
const GITLAB_DUO_WORKFLOW_GOAL_HARD_OVERFLOW_BYTES = 2_000_000;
|
|
110
111
|
|
|
111
112
|
// An overflow-pattern message for an oversized goal. The "prompt is too long" prefix
|
|
112
|
-
// is one of the shared
|
|
113
|
+
// is one of the shared overflow classifier patterns, so
|
|
113
114
|
// `isContextOverflow` recognizes it and the session triggers auto-compaction instead
|
|
114
115
|
// of surfacing a hard failure. Byte counts (not tokens) are reported because the
|
|
115
116
|
// budget is a byte budget.
|
|
@@ -953,7 +954,7 @@ async function runGitLabDuoWorkflow(
|
|
|
953
954
|
state: GitLabDuoWorkflowStreamState,
|
|
954
955
|
): Promise<void> {
|
|
955
956
|
const apiKey = options.apiKey;
|
|
956
|
-
if (!apiKey) throw new
|
|
957
|
+
if (!apiKey) throw new AIError.MissingApiKeyError("gitlab-duo-agent");
|
|
957
958
|
const baseUrl = normalizeGitLabBaseUrl(model.baseUrl || DEFAULT_GITLAB_BASE_URL);
|
|
958
959
|
const fetchImpl = options.fetch ?? fetch;
|
|
959
960
|
const providerSessionState = getGitLabDuoWorkflowProviderSessionState(
|
|
@@ -1604,16 +1605,20 @@ async function requestGitLabDuoWorkflowDirectAccess(
|
|
|
1604
1605
|
// when the assistant error exposes `errorStatus` or the message embeds an
|
|
1605
1606
|
// `HTTP <status>` token. A 401 `{"message":"Unauthorized"}` or a 429 quota
|
|
1606
1607
|
// body would otherwise surface as a hard failure with no recoverable status.
|
|
1607
|
-
throw new
|
|
1608
|
+
throw new AIError.GitLabDuoWorkflowApiError(
|
|
1608
1609
|
message
|
|
1609
1610
|
? `GitLab Duo Workflow direct_access failed with HTTP ${response.status}: ${message}`
|
|
1610
1611
|
: `GitLab Duo Workflow direct_access failed with HTTP ${response.status}`,
|
|
1612
|
+
response.status,
|
|
1611
1613
|
);
|
|
1612
1614
|
}
|
|
1613
1615
|
const payload = (await response.json()) as GitLabDirectAccessResponse;
|
|
1614
1616
|
const token = extractGitLabWorkflowToken(payload);
|
|
1615
1617
|
if (!token) {
|
|
1616
|
-
throw new
|
|
1618
|
+
throw new AIError.ProviderResponseError("GitLab Duo Workflow direct_access did not return credentials", {
|
|
1619
|
+
provider: "gitlab-duo-agent",
|
|
1620
|
+
kind: "empty-body",
|
|
1621
|
+
});
|
|
1617
1622
|
}
|
|
1618
1623
|
traceGitLabDuoWorkflow("direct_access.token", { hasToken: true });
|
|
1619
1624
|
const serviceEndpoint = !payload.gitlab_rails?.token && Boolean(payload.duo_workflow_service?.base_url);
|
|
@@ -1658,12 +1663,18 @@ async function createGitLabDuoWorkflow(
|
|
|
1658
1663
|
hasProjectId: Boolean(projectId),
|
|
1659
1664
|
});
|
|
1660
1665
|
if (!response.ok) {
|
|
1661
|
-
throw new
|
|
1666
|
+
throw new AIError.GitLabDuoWorkflowApiError(
|
|
1667
|
+
`GitLab Duo Workflow create failed with HTTP ${response.status}`,
|
|
1668
|
+
response.status,
|
|
1669
|
+
);
|
|
1662
1670
|
}
|
|
1663
1671
|
const payload = (await response.json()) as GitLabCreateWorkflowResponse;
|
|
1664
1672
|
const workflowId = payload.id ?? payload.workflow_id ?? payload.workflowId;
|
|
1665
1673
|
if (workflowId === undefined) {
|
|
1666
|
-
throw new
|
|
1674
|
+
throw new AIError.ProviderResponseError(
|
|
1675
|
+
`GitLab Duo Workflow create response missing workflow id (HTTP ${response.status})`,
|
|
1676
|
+
{ provider: "gitlab-duo-agent", kind: "empty-body" },
|
|
1677
|
+
);
|
|
1667
1678
|
}
|
|
1668
1679
|
traceGitLabDuoWorkflow("workflow.create.id", { workflowId });
|
|
1669
1680
|
return String(workflowId);
|
|
@@ -1815,7 +1826,7 @@ export function runGitLabDuoWorkflowSocket(
|
|
|
1815
1826
|
};
|
|
1816
1827
|
const abort = (): void => {
|
|
1817
1828
|
close();
|
|
1818
|
-
settle("closed", new
|
|
1829
|
+
settle("closed", new AIError.AbortError("GitLab Duo Workflow request aborted"));
|
|
1819
1830
|
};
|
|
1820
1831
|
if (options.signal?.aborted) {
|
|
1821
1832
|
abort();
|
|
@@ -1856,7 +1867,13 @@ export function runGitLabDuoWorkflowSocket(
|
|
|
1856
1867
|
ws.onerror = event => {
|
|
1857
1868
|
const detail = describeGitLabDuoWorkflowSocketEvent(event);
|
|
1858
1869
|
traceGitLabDuoWorkflow("websocket.error", { event: detail });
|
|
1859
|
-
settle(
|
|
1870
|
+
settle(
|
|
1871
|
+
"closed",
|
|
1872
|
+
new AIError.ProviderResponseError(`GitLab Duo Workflow WebSocket error: ${detail}`, {
|
|
1873
|
+
provider: "gitlab-duo-agent",
|
|
1874
|
+
kind: "runtime",
|
|
1875
|
+
}),
|
|
1876
|
+
);
|
|
1860
1877
|
};
|
|
1861
1878
|
ws.onclose = event => {
|
|
1862
1879
|
traceGitLabDuoWorkflow("websocket.close", { code: event.code, reason: event.reason });
|
|
@@ -2775,7 +2792,10 @@ export async function resolveGitLabDuoWorkflowNamespaceSelection(
|
|
|
2775
2792
|
cwd: options.cwd,
|
|
2776
2793
|
});
|
|
2777
2794
|
} catch (error) {
|
|
2778
|
-
throw new
|
|
2795
|
+
throw new AIError.ProviderResponseError(
|
|
2796
|
+
`GitLab Duo Workflow runtime namespace resolution failed: ${gitLabDuoWorkflowErrorText(error)}`,
|
|
2797
|
+
{ provider: "gitlab-duo-agent", kind: "runtime" },
|
|
2798
|
+
);
|
|
2779
2799
|
}
|
|
2780
2800
|
}
|
|
2781
2801
|
|
|
@@ -3008,7 +3028,7 @@ function requireGitLabDuoWorkflowRequestID(
|
|
|
3008
3028
|
source: Record<string, unknown>,
|
|
3009
3029
|
): string {
|
|
3010
3030
|
if (requestID) return requestID;
|
|
3011
|
-
throw new
|
|
3031
|
+
throw new AIError.ValidationError(
|
|
3012
3032
|
`GitLab Duo Workflow action "${actionName}" missing requestID (keys: ${Object.keys(source).slice(0, 20).join(", ")})`,
|
|
3013
3033
|
);
|
|
3014
3034
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
2
|
+
import * as AIError from "../error";
|
|
2
3
|
import { ANTHROPIC_THINKING, mapAnthropicToolChoice } from "../stream";
|
|
3
4
|
import type { Api, Context, FetchImpl, Model, ModelSpec, SimpleStreamOptions } from "../types";
|
|
4
5
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
@@ -198,17 +199,29 @@ async function getDirectAccessToken(
|
|
|
198
199
|
if (!response.ok) {
|
|
199
200
|
const detail = await response.text();
|
|
200
201
|
if (response.status === 403) {
|
|
201
|
-
throw new
|
|
202
|
+
throw new AIError.ProviderResponseError(
|
|
203
|
+
`GitLab Duo access denied. Ensure Duo is enabled for this account. ${detail}`,
|
|
204
|
+
{ provider: "gitlab-duo", kind: "runtime" },
|
|
205
|
+
);
|
|
202
206
|
}
|
|
203
|
-
throw new
|
|
207
|
+
throw new AIError.GitLabDuoApiError(
|
|
208
|
+
`Failed to get GitLab Duo direct access token: ${response.status} ${detail}`,
|
|
209
|
+
response.status,
|
|
210
|
+
);
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
const payload = (await response.json()) as { token?: string; headers?: Record<string, string> };
|
|
207
214
|
if (!payload.token || typeof payload.token !== "string") {
|
|
208
|
-
throw new
|
|
215
|
+
throw new AIError.ProviderResponseError("GitLab Duo direct access response missing token", {
|
|
216
|
+
provider: "gitlab-duo",
|
|
217
|
+
kind: "envelope",
|
|
218
|
+
});
|
|
209
219
|
}
|
|
210
220
|
if (!payload.headers || typeof payload.headers !== "object") {
|
|
211
|
-
throw new
|
|
221
|
+
throw new AIError.ProviderResponseError("GitLab Duo direct access response missing headers", {
|
|
222
|
+
provider: "gitlab-duo",
|
|
223
|
+
kind: "envelope",
|
|
224
|
+
});
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
const token: DirectAccessToken = {
|
|
@@ -239,12 +252,15 @@ export function streamGitLabDuo(
|
|
|
239
252
|
try {
|
|
240
253
|
const apiKey = typeof options?.apiKey === "string" ? options.apiKey : undefined;
|
|
241
254
|
if (!apiKey || !options) {
|
|
242
|
-
throw new
|
|
255
|
+
throw new AIError.MissingApiKeyError(
|
|
256
|
+
undefined,
|
|
257
|
+
"Missing GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN.",
|
|
258
|
+
);
|
|
243
259
|
}
|
|
244
260
|
|
|
245
261
|
const mapping = getModelMapping(model.id);
|
|
246
262
|
if (!mapping) {
|
|
247
|
-
throw new
|
|
263
|
+
throw new AIError.ConfigurationError(`Unsupported GitLab Duo model: ${model.id}`);
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
const directAccess = await getDirectAccessToken(apiKey, options.fetch);
|
|
@@ -16,6 +16,7 @@ import { Buffer } from "node:buffer";
|
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import { $envpos, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
19
|
+
import * as AIError from "../error";
|
|
19
20
|
import type { FetchImpl } from "../types";
|
|
20
21
|
import { raceWithSignal } from "../utils/abort";
|
|
21
22
|
|
|
@@ -83,7 +84,7 @@ async function loadAdcCredentials(): Promise<{ source: string; creds: AdcFileCre
|
|
|
83
84
|
if (gacPath) {
|
|
84
85
|
const creds = await readJsonFile<AdcFileCredentials>(gacPath);
|
|
85
86
|
if (!creds) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new AIError.ConfigurationError(`GOOGLE_APPLICATION_CREDENTIALS points to a missing file: ${gacPath}`);
|
|
87
88
|
}
|
|
88
89
|
return { source: `gac:${gacPath}`, creds };
|
|
89
90
|
}
|
|
@@ -103,7 +104,7 @@ function pemToPkcs8(pem: string): Uint8Array<ArrayBuffer> {
|
|
|
103
104
|
.replace(/-----BEGIN [^-]+-----/g, "")
|
|
104
105
|
.replace(/-----END [^-]+-----/g, "")
|
|
105
106
|
.replace(/\s+/g, "");
|
|
106
|
-
if (!body) throw new
|
|
107
|
+
if (!body) throw new AIError.ConfigurationError("Invalid PEM: empty body");
|
|
107
108
|
return Uint8Array.fromBase64(body);
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -193,7 +194,11 @@ async function postForToken(
|
|
|
193
194
|
});
|
|
194
195
|
if (!response.ok) {
|
|
195
196
|
const detail = await response.text().catch(() => "");
|
|
196
|
-
throw new
|
|
197
|
+
throw new AIError.OAuthError(`Google OAuth token exchange failed (${response.status}): ${detail}`, {
|
|
198
|
+
kind: "token-exchange",
|
|
199
|
+
provider: "google-vertex",
|
|
200
|
+
status: response.status,
|
|
201
|
+
});
|
|
197
202
|
}
|
|
198
203
|
return (await response.json()) as TokenResponse;
|
|
199
204
|
}
|
|
@@ -239,7 +244,11 @@ async function resolveAccessTokenUncached(
|
|
|
239
244
|
);
|
|
240
245
|
if (!response.ok) {
|
|
241
246
|
const detail = await response.text().catch(() => "");
|
|
242
|
-
throw new
|
|
247
|
+
throw new AIError.OAuthError(`Google Impersonation token exchange failed (${response.status}): ${detail}`, {
|
|
248
|
+
kind: "token-exchange",
|
|
249
|
+
provider: "google-vertex",
|
|
250
|
+
status: response.status,
|
|
251
|
+
});
|
|
243
252
|
}
|
|
244
253
|
const data = (await response.json()) as { accessToken: string; expireTime: string };
|
|
245
254
|
const expiresIn = Math.max(0, Math.floor((new Date(data.expireTime).getTime() - Date.now()) / 1000));
|
|
@@ -254,7 +263,8 @@ async function resolveAccessTokenUncached(
|
|
|
254
263
|
}
|
|
255
264
|
const metadata = await fetchMetadataToken(signal, fetchImpl);
|
|
256
265
|
if (metadata) return { source: "metadata", token: metadata };
|
|
257
|
-
throw new
|
|
266
|
+
throw new AIError.MissingApiKeyError(
|
|
267
|
+
undefined,
|
|
258
268
|
"Vertex AI requires Application Default Credentials. Set GOOGLE_APPLICATION_CREDENTIALS, run `gcloud auth application-default login`, or run on a GCE/Cloud Run instance with a service account.",
|
|
259
269
|
);
|
|
260
270
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
15
15
|
import { extractHttpStatusFromError, fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
|
|
16
16
|
import { type } from "arktype";
|
|
17
|
-
import
|
|
17
|
+
import * as AIError from "../error";
|
|
18
18
|
import type {
|
|
19
19
|
Api,
|
|
20
20
|
AssistantMessage,
|
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
import { normalizeSystemPrompts } from "../utils";
|
|
31
31
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
32
32
|
import { extractGoogleValidationUrl, formatGoogleValidationRequiredMessage } from "../utils/google-validation";
|
|
33
|
-
import {
|
|
33
|
+
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
34
34
|
import { armPreResponseTimeout, getStreamFirstEventTimeoutMs } from "../utils/idle-iterator";
|
|
35
35
|
// Refresh is the sole responsibility of AuthStorage (broker-aware, single-flighted);
|
|
36
36
|
// the stream provider trusts the access token threaded through `options.apiKey`.
|
|
@@ -59,11 +59,6 @@ import {
|
|
|
59
59
|
*/
|
|
60
60
|
export type { GoogleThinkingLevel };
|
|
61
61
|
|
|
62
|
-
/** Non-2xx response (or in-stream error chunk) from the Cloud Code Assist API. */
|
|
63
|
-
export class GeminiCliApiError extends ProviderHttpError {
|
|
64
|
-
override readonly name = "GeminiCliApiError";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
62
|
function isPlanningLeakPrefix(text: string): boolean {
|
|
68
63
|
const trimmed = text.trimStart();
|
|
69
64
|
if (!trimmed.startsWith("{")) {
|
|
@@ -345,22 +340,6 @@ function shouldInjectAntigravitySystemInstruction(modelId: string): boolean {
|
|
|
345
340
|
return normalized.includes("claude") || normalized.includes("gemini-3");
|
|
346
341
|
}
|
|
347
342
|
|
|
348
|
-
/**
|
|
349
|
-
* Extract a clean, user-friendly error message from Google API error response.
|
|
350
|
-
* Parses JSON error responses and returns just the message field.
|
|
351
|
-
*/
|
|
352
|
-
function extractErrorMessage(errorText: string): string {
|
|
353
|
-
try {
|
|
354
|
-
const parsed = JSON.parse(errorText) as { error?: { message?: string } };
|
|
355
|
-
if (parsed.error?.message) {
|
|
356
|
-
return parsed.error.message;
|
|
357
|
-
}
|
|
358
|
-
} catch {
|
|
359
|
-
// Not JSON, return as-is
|
|
360
|
-
}
|
|
361
|
-
return errorText;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
343
|
const optionalCredentialString = type("unknown").pipe(raw => {
|
|
365
344
|
const out = type("string")(raw);
|
|
366
345
|
return out instanceof type.errors ? undefined : out;
|
|
@@ -406,16 +385,16 @@ export function parseGeminiCliCredentials(apiKeyRaw: string): ParsedGeminiCliCre
|
|
|
406
385
|
try {
|
|
407
386
|
rawCredentials = JSON.parse(apiKeyRaw);
|
|
408
387
|
} catch {
|
|
409
|
-
throw new
|
|
388
|
+
throw new AIError.ValidationError(invalidCredentialsMessage);
|
|
410
389
|
}
|
|
411
390
|
const parsed = geminiCliCredentialsSchema(rawCredentials);
|
|
412
391
|
if (parsed instanceof type.errors) {
|
|
413
|
-
throw new
|
|
392
|
+
throw new AIError.ValidationError(invalidCredentialsMessage);
|
|
414
393
|
}
|
|
415
394
|
|
|
416
395
|
const projectId = parsed.projectId ?? parsed.project_id;
|
|
417
396
|
if (parsed.token === undefined || projectId === undefined) {
|
|
418
|
-
throw new
|
|
397
|
+
throw new AIError.ValidationError(missingCredentialsMessage);
|
|
419
398
|
}
|
|
420
399
|
|
|
421
400
|
const refreshToken = parsed.refreshToken ?? parsed.refresh;
|
|
@@ -517,7 +496,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
517
496
|
const stream = new AssistantMessageEventStream();
|
|
518
497
|
|
|
519
498
|
(async () => {
|
|
520
|
-
const startTime =
|
|
499
|
+
const startTime = performance.now();
|
|
521
500
|
let firstTokenTime: number | undefined;
|
|
522
501
|
|
|
523
502
|
const output: AssistantMessage = {
|
|
@@ -542,7 +521,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
542
521
|
try {
|
|
543
522
|
const apiKeyRaw = options?.apiKey;
|
|
544
523
|
if (!apiKeyRaw) {
|
|
545
|
-
throw new
|
|
524
|
+
throw new AIError.ConfigurationError(
|
|
525
|
+
"Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.",
|
|
526
|
+
);
|
|
546
527
|
}
|
|
547
528
|
|
|
548
529
|
const isAntigravity = model.provider === "google-antigravity";
|
|
@@ -558,8 +539,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
558
539
|
parsedCredentials.expiresAt !== undefined &&
|
|
559
540
|
Date.now() >= parsedCredentials.expiresAt
|
|
560
541
|
) {
|
|
561
|
-
throw new
|
|
542
|
+
throw new AIError.OAuthError(
|
|
562
543
|
"OAuth token expired before request — please retry; AuthStorage will refresh on the next attempt.",
|
|
544
|
+
{ kind: "token-refresh", provider: model.provider },
|
|
563
545
|
);
|
|
564
546
|
}
|
|
565
547
|
const baseUrl = model.baseUrl?.trim();
|
|
@@ -646,7 +628,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
646
628
|
let lastResponseId: string | undefined;
|
|
647
629
|
const ensureStarted = () => {
|
|
648
630
|
if (!started) {
|
|
649
|
-
if (!firstTokenTime) firstTokenTime =
|
|
631
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
650
632
|
stream.push({ type: "start", partial: output });
|
|
651
633
|
started = true;
|
|
652
634
|
}
|
|
@@ -670,7 +652,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
670
652
|
|
|
671
653
|
const streamResponse = async (activeResponse: Response): Promise<boolean> => {
|
|
672
654
|
if (!activeResponse.body) {
|
|
673
|
-
throw new
|
|
655
|
+
throw new AIError.ProviderResponseError("No response body", {
|
|
656
|
+
provider: model.provider,
|
|
657
|
+
kind: "empty-body",
|
|
658
|
+
});
|
|
674
659
|
}
|
|
675
660
|
|
|
676
661
|
// Scoped per attempt so a failed/empty retry cannot leak its
|
|
@@ -706,16 +691,17 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
706
691
|
const detail = chunk.error.message || chunk.error.status || "unknown error";
|
|
707
692
|
const message = `Cloud Code Assist stream error: ${detail}`;
|
|
708
693
|
throw typeof chunk.error.code === "number" && chunk.error.code >= 400
|
|
709
|
-
? new GeminiCliApiError(message, chunk.error.code)
|
|
710
|
-
: new
|
|
694
|
+
? new AIError.GeminiCliApiError(message, chunk.error.code)
|
|
695
|
+
: new AIError.ProviderResponseError(message, { provider: model.provider, kind: "runtime" });
|
|
711
696
|
}
|
|
712
697
|
const responseData = chunk.response;
|
|
713
698
|
if (!responseData) continue;
|
|
714
699
|
if (responseData.responseId) lastResponseId = responseData.responseId;
|
|
715
700
|
if (!responseData.candidates?.length && responseData.promptFeedback?.blockReason) {
|
|
716
701
|
const detail = responseData.promptFeedback.blockReasonMessage;
|
|
717
|
-
throw new
|
|
702
|
+
throw new AIError.ProviderResponseError(
|
|
718
703
|
`Request blocked by Google (${responseData.promptFeedback.blockReason})${detail ? `: ${detail}` : ""}`,
|
|
704
|
+
{ provider: model.provider, kind: "content-blocked" },
|
|
719
705
|
);
|
|
720
706
|
}
|
|
721
707
|
|
|
@@ -908,7 +894,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
908
894
|
}
|
|
909
895
|
|
|
910
896
|
if (!response.ok) {
|
|
911
|
-
if (
|
|
897
|
+
if (AIError.isTransientStatus(response.status)) {
|
|
912
898
|
if (!isLastEndpoint) {
|
|
913
899
|
continue;
|
|
914
900
|
}
|
|
@@ -921,8 +907,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
921
907
|
"retry your request",
|
|
922
908
|
parsedCredentials.email,
|
|
923
909
|
)
|
|
924
|
-
:
|
|
925
|
-
throw new GeminiCliApiError(
|
|
910
|
+
: errorText;
|
|
911
|
+
throw new AIError.GeminiCliApiError(
|
|
926
912
|
`Cloud Code Assist API error (${response.status}): ${errorMessage}`,
|
|
927
913
|
response.status,
|
|
928
914
|
{ headers: response.headers },
|
|
@@ -934,7 +920,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
934
920
|
|
|
935
921
|
for (let emptyAttempt = 0; emptyAttempt <= MAX_EMPTY_STREAM_RETRIES; emptyAttempt++) {
|
|
936
922
|
if (options?.signal?.aborted) {
|
|
937
|
-
throw new
|
|
923
|
+
throw new AIError.AbortError("Request was aborted");
|
|
938
924
|
}
|
|
939
925
|
|
|
940
926
|
if (emptyAttempt > 0) {
|
|
@@ -942,11 +928,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
942
928
|
try {
|
|
943
929
|
await scheduler.wait(backoffMs, { signal: options?.signal });
|
|
944
930
|
} catch {
|
|
945
|
-
throw new
|
|
931
|
+
throw new AIError.AbortError("Request was aborted");
|
|
946
932
|
}
|
|
947
933
|
|
|
948
934
|
if (!requestUrl) {
|
|
949
|
-
throw new
|
|
935
|
+
throw new AIError.ConfigurationError("Missing request URL");
|
|
950
936
|
}
|
|
951
937
|
|
|
952
938
|
currentResponse = await (options?.fetch ?? fetch)(requestUrl, {
|
|
@@ -958,7 +944,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
958
944
|
|
|
959
945
|
if (!currentResponse.ok) {
|
|
960
946
|
const retryErrorText = await currentResponse.text();
|
|
961
|
-
throw new GeminiCliApiError(
|
|
947
|
+
throw new AIError.GeminiCliApiError(
|
|
962
948
|
`Cloud Code Assist API error (${currentResponse.status}): ${retryErrorText}`,
|
|
963
949
|
currentResponse.status,
|
|
964
950
|
{ headers: currentResponse.headers },
|
|
@@ -978,16 +964,20 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
978
964
|
}
|
|
979
965
|
|
|
980
966
|
if (!receivedContent) {
|
|
981
|
-
throw new
|
|
967
|
+
throw new AIError.ProviderResponseError("Cloud Code Assist API returned an empty response", {
|
|
968
|
+
provider: model.provider,
|
|
969
|
+
kind: "empty-body",
|
|
970
|
+
});
|
|
982
971
|
}
|
|
983
972
|
|
|
984
973
|
if (options?.signal?.aborted) {
|
|
985
|
-
throw new
|
|
974
|
+
throw new AIError.AbortError("Request was aborted");
|
|
986
975
|
}
|
|
987
976
|
|
|
988
977
|
if (!sawFinishReason) {
|
|
989
|
-
throw new
|
|
978
|
+
throw new AIError.ProviderResponseError(
|
|
990
979
|
"Cloud Code Assist stream ended without a finish reason (connection dropped or response truncated)",
|
|
980
|
+
{ provider: model.provider, kind: "incomplete-stream" },
|
|
991
981
|
);
|
|
992
982
|
}
|
|
993
983
|
|
|
@@ -1007,7 +997,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1007
997
|
break;
|
|
1008
998
|
} catch (error) {
|
|
1009
999
|
const status = extractHttpStatusFromError(error);
|
|
1010
|
-
if (
|
|
1000
|
+
if (AIError.isTransientStatus(status)) {
|
|
1011
1001
|
if (!isLastEndpoint && !started) {
|
|
1012
1002
|
continue;
|
|
1013
1003
|
}
|
|
@@ -1017,27 +1007,23 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1017
1007
|
}
|
|
1018
1008
|
|
|
1019
1009
|
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
1020
|
-
throw new
|
|
1010
|
+
throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
|
|
1011
|
+
provider: model.provider,
|
|
1012
|
+
kind: "output",
|
|
1013
|
+
});
|
|
1021
1014
|
}
|
|
1022
1015
|
|
|
1023
|
-
output.duration =
|
|
1016
|
+
output.duration = performance.now() - startTime;
|
|
1024
1017
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1025
1018
|
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
1026
1019
|
stream.end();
|
|
1027
1020
|
} catch (error) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
output.
|
|
1034
|
-
output.errorStatus = extractHttpStatusFromError(error);
|
|
1035
|
-
output.errorMessage = await appendRawHttpRequestDumpFor400(
|
|
1036
|
-
error instanceof Error ? error.message : JSON.stringify(error),
|
|
1037
|
-
error,
|
|
1038
|
-
rawRequestDump,
|
|
1039
|
-
);
|
|
1040
|
-
output.duration = Date.now() - startTime;
|
|
1021
|
+
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal, rawRequestDump });
|
|
1022
|
+
output.stopReason = result.stopReason;
|
|
1023
|
+
output.errorStatus = result.status;
|
|
1024
|
+
output.errorId = result.id;
|
|
1025
|
+
output.errorMessage = result.message;
|
|
1026
|
+
output.duration = performance.now() - startTime;
|
|
1041
1027
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
1042
1028
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
1043
1029
|
stream.end();
|