@oh-my-pi/pi-ai 16.2.1 → 16.2.3
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 +36 -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 +3 -2
- 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/usage/openai-codex-base-url.d.ts +17 -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/stream-markup-healing.d.ts +14 -5
- 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 +40 -30
- 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 +50 -24
- 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/usage/openai-codex-base-url.ts +30 -10
- 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/stream-markup-healing.ts +41 -18
- 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,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { scheduler } from "node:timers/promises";
|
|
6
6
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { renderDemotedThinking } from "../dialect/demotion";
|
|
9
|
+
import * as AIError from "../error";
|
|
9
10
|
import type {
|
|
10
11
|
Api,
|
|
11
12
|
AssistantMessage,
|
|
@@ -22,7 +23,7 @@ import type {
|
|
|
22
23
|
} from "../types";
|
|
23
24
|
import { normalizeSystemPrompts } from "../utils";
|
|
24
25
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
25
|
-
import {
|
|
26
|
+
import type { RawHttpRequestDump } from "../utils/http-inspector";
|
|
26
27
|
import { normalizeSchemaForCCA, normalizeSchemaForGoogle, toolWireSchema } from "../utils/schema";
|
|
27
28
|
import type {
|
|
28
29
|
Content,
|
|
@@ -47,11 +48,6 @@ export type {
|
|
|
47
48
|
} from "./google-types";
|
|
48
49
|
export { normalizeSchemaForGoogle };
|
|
49
50
|
|
|
50
|
-
/** Non-2xx response (or in-stream error chunk) from the Google Generative Language / Vertex API. */
|
|
51
|
-
export class GoogleApiError extends ProviderHttpError {
|
|
52
|
-
override readonly name = "GoogleApiError";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
51
|
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
|
|
56
52
|
|
|
57
53
|
/**
|
|
@@ -234,18 +230,16 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
234
230
|
} else if (block.type === "thinking") {
|
|
235
231
|
// Skip empty thinking blocks
|
|
236
232
|
if (!block.thinking || block.thinking.trim() === "") continue;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (isSameProviderAndModel) {
|
|
240
|
-
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature);
|
|
233
|
+
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature);
|
|
234
|
+
if (thoughtSignature) {
|
|
241
235
|
parts.push({
|
|
242
236
|
thought: true,
|
|
243
237
|
text: block.thinking.toWellFormed(),
|
|
244
|
-
|
|
238
|
+
thoughtSignature,
|
|
245
239
|
});
|
|
246
240
|
} else {
|
|
247
241
|
parts.push({
|
|
248
|
-
text: block.thinking
|
|
242
|
+
text: renderDemotedThinking(model.id, block.thinking),
|
|
249
243
|
});
|
|
250
244
|
}
|
|
251
245
|
} else if (block.type === "toolCall") {
|
|
@@ -420,7 +414,7 @@ export function mapStopReason(reason: FinishReason): StopReason {
|
|
|
420
414
|
case "NO_IMAGE":
|
|
421
415
|
return "error";
|
|
422
416
|
default: {
|
|
423
|
-
throw new
|
|
417
|
+
throw new AIError.ConfigurationError(`Unhandled stop reason: ${reason satisfies never}`);
|
|
424
418
|
}
|
|
425
419
|
}
|
|
426
420
|
}
|
|
@@ -597,13 +591,14 @@ export async function consumeGoogleStream<T extends GoogleApiType>(args: {
|
|
|
597
591
|
const detail = chunk.error.message || chunk.error.status || "unknown error";
|
|
598
592
|
const message = `Google API stream error: ${detail}`;
|
|
599
593
|
throw typeof chunk.error.code === "number" && chunk.error.code >= 400
|
|
600
|
-
? new GoogleApiError(message, chunk.error.code)
|
|
601
|
-
: new
|
|
594
|
+
? new AIError.GoogleApiError(message, chunk.error.code)
|
|
595
|
+
: new AIError.ProviderResponseError(message, { provider: model.provider, kind: "output" });
|
|
602
596
|
}
|
|
603
597
|
if (!chunk.candidates?.length && chunk.promptFeedback?.blockReason) {
|
|
604
598
|
const detail = chunk.promptFeedback.blockReasonMessage;
|
|
605
|
-
throw new
|
|
599
|
+
throw new AIError.ProviderResponseError(
|
|
606
600
|
`Request blocked by Google (${chunk.promptFeedback.blockReason})${detail ? `: ${detail}` : ""}`,
|
|
601
|
+
{ provider: model.provider, kind: "content-blocked" },
|
|
607
602
|
);
|
|
608
603
|
}
|
|
609
604
|
const candidate = chunk.candidates?.[0];
|
|
@@ -734,15 +729,21 @@ export async function consumeGoogleStream<T extends GoogleApiType>(args: {
|
|
|
734
729
|
flushCurrent();
|
|
735
730
|
|
|
736
731
|
if (options?.signal?.aborted) {
|
|
737
|
-
throw new
|
|
732
|
+
throw new AIError.AbortError();
|
|
738
733
|
}
|
|
739
734
|
|
|
740
735
|
if (!sawFinishReason) {
|
|
741
|
-
throw new
|
|
736
|
+
throw new AIError.ProviderResponseError(
|
|
737
|
+
"Google API stream ended without a finish reason (connection dropped or response truncated)",
|
|
738
|
+
{ provider: model.provider, kind: "incomplete-stream" },
|
|
739
|
+
);
|
|
742
740
|
}
|
|
743
741
|
|
|
744
742
|
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
745
|
-
throw new
|
|
743
|
+
throw new AIError.ProviderResponseError(output.errorMessage ?? "An unknown error occurred", {
|
|
744
|
+
provider: model.provider,
|
|
745
|
+
kind: "output",
|
|
746
|
+
});
|
|
746
747
|
}
|
|
747
748
|
}
|
|
748
749
|
|
|
@@ -826,7 +827,7 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
|
|
|
826
827
|
|
|
827
828
|
if (options.signal) {
|
|
828
829
|
if (options.signal.aborted) {
|
|
829
|
-
throw new
|
|
830
|
+
throw new AIError.AbortError("Request aborted");
|
|
830
831
|
}
|
|
831
832
|
config.abortSignal = options.signal;
|
|
832
833
|
}
|
|
@@ -864,7 +865,7 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
864
865
|
const stream = new AssistantMessageEventStream();
|
|
865
866
|
|
|
866
867
|
(async () => {
|
|
867
|
-
const startTime =
|
|
868
|
+
const startTime = performance.now();
|
|
868
869
|
let firstTokenTime: number | undefined;
|
|
869
870
|
|
|
870
871
|
const output: AssistantMessage = {
|
|
@@ -914,14 +915,17 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
914
915
|
});
|
|
915
916
|
if (!response.ok) {
|
|
916
917
|
const errorText = await response.text().catch(() => "");
|
|
917
|
-
throw new GoogleApiError(
|
|
918
|
+
throw new AIError.GoogleApiError(
|
|
918
919
|
`Google API error (${response.status}): ${extractGoogleErrorMessage(errorText)}`,
|
|
919
920
|
response.status,
|
|
920
921
|
{ headers: response.headers },
|
|
921
922
|
);
|
|
922
923
|
}
|
|
923
924
|
if (!response.body) {
|
|
924
|
-
throw new
|
|
925
|
+
throw new AIError.ProviderResponseError("Google API returned an empty response body", {
|
|
926
|
+
provider: model.provider,
|
|
927
|
+
kind: "empty-body",
|
|
928
|
+
});
|
|
925
929
|
}
|
|
926
930
|
return response.body as ReadableStream<Uint8Array>;
|
|
927
931
|
};
|
|
@@ -944,39 +948,37 @@ export function streamGoogleGenAI<T extends "google-generative-ai" | "google-ver
|
|
|
944
948
|
options,
|
|
945
949
|
retainTextSignature,
|
|
946
950
|
onFirstToken: () => {
|
|
947
|
-
firstTokenTime =
|
|
951
|
+
firstTokenTime = performance.now();
|
|
948
952
|
},
|
|
949
953
|
});
|
|
950
954
|
|
|
951
955
|
if (output.stopReason !== "stop" || hasMeaningfulGoogleContent(output)) break;
|
|
952
956
|
if (emptyAttempt >= MAX_EMPTY_STREAM_RETRIES) {
|
|
953
|
-
throw new
|
|
957
|
+
throw new AIError.ProviderResponseError(
|
|
954
958
|
`Google API returned an empty response (finishReason STOP with no content) after ${MAX_EMPTY_STREAM_RETRIES + 1} attempts`,
|
|
959
|
+
{ provider: model.provider, kind: "empty-body" },
|
|
955
960
|
);
|
|
956
961
|
}
|
|
957
962
|
try {
|
|
958
963
|
await scheduler.wait(EMPTY_STREAM_BASE_DELAY_MS * 2 ** emptyAttempt, { signal: options?.signal });
|
|
959
964
|
} catch {
|
|
960
|
-
throw new
|
|
965
|
+
throw new AIError.AbortError();
|
|
961
966
|
}
|
|
962
967
|
resetGoogleStreamOutputForRetry(output);
|
|
963
968
|
body = await openStream();
|
|
964
969
|
}
|
|
965
970
|
|
|
966
|
-
output.duration =
|
|
971
|
+
output.duration = performance.now() - startTime;
|
|
967
972
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
968
973
|
stream.push({ type: "done", reason: output.stopReason as "length" | "stop" | "toolUse", message: output });
|
|
969
974
|
stream.end();
|
|
970
975
|
} catch (error) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
output.
|
|
977
|
-
output.errorStatus = extractHttpStatusFromError(error);
|
|
978
|
-
output.errorMessage = await finalizeErrorMessage(error, rawRequestDump);
|
|
979
|
-
output.duration = Date.now() - startTime;
|
|
976
|
+
const result = await AIError.finalize(error, { api: model.api, signal: options?.signal, rawRequestDump });
|
|
977
|
+
output.stopReason = result.stopReason;
|
|
978
|
+
output.errorStatus = result.status;
|
|
979
|
+
output.errorId = result.id;
|
|
980
|
+
output.errorMessage = result.message;
|
|
981
|
+
output.duration = performance.now() - startTime;
|
|
980
982
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
981
983
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
982
984
|
stream.end();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import * as AIError from "../error";
|
|
2
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
3
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
4
5
|
import { getVertexAccessToken } from "./google-auth";
|
|
@@ -69,7 +70,7 @@ function resolveApiKey(options?: GoogleVertexOptions): string | undefined {
|
|
|
69
70
|
function resolveProject(options?: GoogleVertexOptions): string {
|
|
70
71
|
const project = options?.project || $env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT;
|
|
71
72
|
if (!project) {
|
|
72
|
-
throw new
|
|
73
|
+
throw new AIError.ConfigurationError(
|
|
73
74
|
"Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCP_PROJECT/GCLOUD_PROJECT or pass project in options.",
|
|
74
75
|
);
|
|
75
76
|
}
|
|
@@ -83,7 +84,7 @@ function resolveLocation(options?: GoogleVertexOptions): string {
|
|
|
83
84
|
const location =
|
|
84
85
|
options?.location || $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION;
|
|
85
86
|
if (!location) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new AIError.ConfigurationError(
|
|
87
88
|
"Vertex AI requires a location. Set GOOGLE_VERTEX_LOCATION/GOOGLE_CLOUD_LOCATION/VERTEX_LOCATION or pass location in options.",
|
|
88
89
|
);
|
|
89
90
|
}
|
package/src/providers/google.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as AIError from "../error";
|
|
1
2
|
import { getEnvApiKey } from "../stream";
|
|
2
3
|
import type { Context, Model, StreamFunction } from "../types";
|
|
3
4
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
@@ -24,7 +25,10 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
|
|
|
24
25
|
prepare: (): GoogleGenAIRequestPlan => {
|
|
25
26
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
|
|
26
27
|
if (!apiKey) {
|
|
27
|
-
throw new
|
|
28
|
+
throw new AIError.MissingApiKeyError(
|
|
29
|
+
undefined,
|
|
30
|
+
"Google Generative AI requires an API key (GEMINI_API_KEY or options.apiKey).",
|
|
31
|
+
);
|
|
28
32
|
}
|
|
29
33
|
const params = buildGoogleGenerateContentParams(model, context, options ?? {});
|
|
30
34
|
// `model.baseUrl` already includes the API version segment when set (mirrors the
|
package/src/providers/mock.ts
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { registerCustomApi } from "../api-registry";
|
|
46
|
+
import * as AIError from "../error";
|
|
46
47
|
import type {
|
|
47
48
|
Api,
|
|
48
49
|
AssistantMessage,
|
|
@@ -242,7 +243,7 @@ export function streamMock(
|
|
|
242
243
|
if (!isMockModel(model)) {
|
|
243
244
|
queueMicrotask(() => {
|
|
244
245
|
stream.fail(
|
|
245
|
-
new
|
|
246
|
+
new AIError.ValidationError(
|
|
246
247
|
"streamMock called with a model not produced by createMockModel(). " + "Pass a MockModel instance.",
|
|
247
248
|
),
|
|
248
249
|
);
|
|
@@ -288,6 +289,7 @@ async function runMock(
|
|
|
288
289
|
options: SimpleStreamOptions | undefined,
|
|
289
290
|
): Promise<void> {
|
|
290
291
|
const startedAt = Date.now();
|
|
292
|
+
const perfStart = performance.now();
|
|
291
293
|
|
|
292
294
|
let handler: MockHandler | undefined;
|
|
293
295
|
try {
|
|
@@ -299,7 +301,7 @@ async function runMock(
|
|
|
299
301
|
|
|
300
302
|
if (handler === undefined) {
|
|
301
303
|
stream.fail(
|
|
302
|
-
new
|
|
304
|
+
new AIError.ValidationError(
|
|
303
305
|
`Mock model "${model.id}" received call ${model.calls.length} but no response or handler is configured.`,
|
|
304
306
|
),
|
|
305
307
|
);
|
|
@@ -338,7 +340,7 @@ async function runMock(
|
|
|
338
340
|
try {
|
|
339
341
|
await sleep(response.delayMs, options?.signal);
|
|
340
342
|
} catch {
|
|
341
|
-
emitTerminalError(stream, model, startedAt, "aborted", "Mock aborted during delay.");
|
|
343
|
+
emitTerminalError(stream, model, startedAt, perfStart, "aborted", "Mock aborted during delay.");
|
|
342
344
|
return;
|
|
343
345
|
}
|
|
344
346
|
}
|
|
@@ -350,7 +352,7 @@ async function runMock(
|
|
|
350
352
|
: response.throw instanceof Error
|
|
351
353
|
? response.throw.message
|
|
352
354
|
: String(response.throw);
|
|
353
|
-
emitTerminalError(stream, model, startedAt, "error", message);
|
|
355
|
+
emitTerminalError(stream, model, startedAt, perfStart, "error", message);
|
|
354
356
|
return;
|
|
355
357
|
}
|
|
356
358
|
|
|
@@ -397,7 +399,7 @@ async function runMock(
|
|
|
397
399
|
partial.stopDetails = response.stopDetails;
|
|
398
400
|
partial.errorMessage = response.errorMessage;
|
|
399
401
|
partial.usage = mergeUsage(response.usage);
|
|
400
|
-
partial.duration =
|
|
402
|
+
partial.duration = performance.now() - perfStart;
|
|
401
403
|
|
|
402
404
|
if (reason === "aborted" || reason === "error") {
|
|
403
405
|
stream.push({
|
|
@@ -460,6 +462,7 @@ function emitTerminalError(
|
|
|
460
462
|
stream: AssistantMessageEventStream,
|
|
461
463
|
model: Model<Api>,
|
|
462
464
|
startedAt: number,
|
|
465
|
+
perfStart: number,
|
|
463
466
|
reason: "aborted" | "error",
|
|
464
467
|
message: string,
|
|
465
468
|
): void {
|
|
@@ -473,7 +476,7 @@ function emitTerminalError(
|
|
|
473
476
|
stopReason: reason as StopReason,
|
|
474
477
|
errorMessage: message,
|
|
475
478
|
timestamp: startedAt,
|
|
476
|
-
duration:
|
|
479
|
+
duration: performance.now() - perfStart,
|
|
477
480
|
};
|
|
478
481
|
stream.push({ type: "start", partial: failure });
|
|
479
482
|
stream.push({ type: "error", reason, error: failure });
|
|
@@ -488,7 +491,7 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
488
491
|
const onAbort = () => {
|
|
489
492
|
clearTimeout(timer);
|
|
490
493
|
signal?.removeEventListener("abort", onAbort);
|
|
491
|
-
reject(signal?.reason ?? new
|
|
494
|
+
reject(signal?.reason ?? new AIError.AbortError("aborted"));
|
|
492
495
|
};
|
|
493
496
|
const timer = setTimeout(() => {
|
|
494
497
|
signal?.removeEventListener("abort", onAbort);
|
package/src/providers/ollama.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { fetchWithRetry, parseStreamingJson } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import * as AIError from "../error";
|
|
3
3
|
import { getEnvApiKey } from "../stream";
|
|
4
4
|
import type {
|
|
5
5
|
Api,
|
|
@@ -15,8 +15,9 @@ import type {
|
|
|
15
15
|
ToolChoice,
|
|
16
16
|
} from "../types";
|
|
17
17
|
import { normalizeSystemPrompts } from "../utils";
|
|
18
|
+
import { clearStreamingPartialJson, kStreamingPartialJson } from "../utils/block-symbols";
|
|
18
19
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
19
|
-
import {
|
|
20
|
+
import type { CapturedHttpErrorResponse, RawHttpRequestDump } from "../utils/http-inspector";
|
|
20
21
|
import {
|
|
21
22
|
armPreResponseTimeout,
|
|
22
23
|
getOpenAIStreamFirstEventTimeoutMs,
|
|
@@ -32,11 +33,6 @@ import {
|
|
|
32
33
|
import { transformMessages } from "./transform-messages";
|
|
33
34
|
import { joinTextWithImagePlaceholder, partitionVisionContent } from "./vision-guard";
|
|
34
35
|
|
|
35
|
-
/** Non-2xx response from the Ollama `/api/chat` endpoint. */
|
|
36
|
-
export class OllamaApiError extends ProviderHttpError {
|
|
37
|
-
override readonly name = "OllamaApiError";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
export interface OllamaChatOptions extends StreamOptions {
|
|
41
37
|
reasoning?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
42
38
|
disableReasoning?: boolean;
|
|
@@ -90,7 +86,7 @@ type OllamaChatChunk = {
|
|
|
90
86
|
|
|
91
87
|
type InternalToolCallBlock = AssistantMessage["content"][number] & {
|
|
92
88
|
type: "toolCall";
|
|
93
|
-
|
|
89
|
+
[kStreamingPartialJson]?: string;
|
|
94
90
|
};
|
|
95
91
|
|
|
96
92
|
function normalizeBaseUrl(baseUrl?: string): string {
|
|
@@ -416,9 +412,9 @@ function endToolCallBlock(stream: AssistantMessageEventStream, output: Assistant
|
|
|
416
412
|
return;
|
|
417
413
|
}
|
|
418
414
|
const toolCall = block as InternalToolCallBlock;
|
|
419
|
-
if (toolCall
|
|
420
|
-
toolCall.arguments = parseStreamingJson<Record<string, unknown>>(toolCall
|
|
421
|
-
|
|
415
|
+
if (toolCall[kStreamingPartialJson]) {
|
|
416
|
+
toolCall.arguments = parseStreamingJson<Record<string, unknown>>(toolCall[kStreamingPartialJson]);
|
|
417
|
+
clearStreamingPartialJson(toolCall);
|
|
422
418
|
}
|
|
423
419
|
stream.push({ type: "toolcall_end", contentIndex: index, toolCall, partial: output });
|
|
424
420
|
}
|
|
@@ -456,7 +452,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
456
452
|
): AssistantMessageEventStream => {
|
|
457
453
|
const stream = new AssistantMessageEventStream();
|
|
458
454
|
void (async () => {
|
|
459
|
-
const startTime =
|
|
455
|
+
const startTime = performance.now();
|
|
460
456
|
let firstTokenTime: number | undefined;
|
|
461
457
|
const output = createEmptyOutput(model);
|
|
462
458
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
@@ -469,6 +465,10 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
469
465
|
? new StreamMarkupHealing({ pattern: streamMarkupHealingPattern })
|
|
470
466
|
: undefined;
|
|
471
467
|
let healedToolCallEmitted = false;
|
|
468
|
+
// Once the provider streams native reasoning (`message.thinking`), drop any
|
|
469
|
+
// thinking the text-channel healer also recovers so a model that emits both
|
|
470
|
+
// does not double-count its reasoning.
|
|
471
|
+
let suppressHealedThinking = false;
|
|
472
472
|
const endActiveTextBlock = (): void => {
|
|
473
473
|
if (activeTextIndex === undefined) return;
|
|
474
474
|
endTextBlock(stream, output, activeTextIndex);
|
|
@@ -497,7 +497,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
497
497
|
partial: output,
|
|
498
498
|
});
|
|
499
499
|
}
|
|
500
|
-
if (!firstTokenTime) firstTokenTime =
|
|
500
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
501
501
|
};
|
|
502
502
|
const appendVisibleThinking = (thinking: string): void => {
|
|
503
503
|
if (thinking.length === 0) return;
|
|
@@ -517,7 +517,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
517
517
|
partial: output,
|
|
518
518
|
});
|
|
519
519
|
}
|
|
520
|
-
if (!firstTokenTime) firstTokenTime =
|
|
520
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
521
521
|
};
|
|
522
522
|
const emitHealedToolCall = (call: HealedToolCall): void => {
|
|
523
523
|
endActiveThinkingBlock();
|
|
@@ -527,7 +527,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
527
527
|
id: call.id,
|
|
528
528
|
name: call.name,
|
|
529
529
|
arguments: parseStreamingJson<Record<string, unknown>>(call.arguments),
|
|
530
|
-
|
|
530
|
+
[kStreamingPartialJson]: call.arguments,
|
|
531
531
|
};
|
|
532
532
|
output.content.push(toolCall);
|
|
533
533
|
const index = output.content.length - 1;
|
|
@@ -540,13 +540,13 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
540
540
|
});
|
|
541
541
|
endToolCallBlock(stream, output, index);
|
|
542
542
|
healedToolCallEmitted = true;
|
|
543
|
-
if (!firstTokenTime) firstTokenTime =
|
|
543
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
544
544
|
};
|
|
545
545
|
const emitHealingEvent = (event: StreamMarkupHealingEvent): void => {
|
|
546
546
|
if (event.type === "text") {
|
|
547
547
|
appendVisibleText(event.text);
|
|
548
548
|
} else if (event.type === "thinking") {
|
|
549
|
-
appendVisibleThinking(event.thinking);
|
|
549
|
+
if (!suppressHealedThinking) appendVisibleThinking(event.thinking);
|
|
550
550
|
} else {
|
|
551
551
|
emitHealedToolCall(event.call);
|
|
552
552
|
}
|
|
@@ -558,7 +558,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
558
558
|
try {
|
|
559
559
|
const apiKey = options.apiKey || getEnvApiKey(model.provider);
|
|
560
560
|
if (!apiKey) {
|
|
561
|
-
throw new
|
|
561
|
+
throw new AIError.MissingApiKeyError(model.provider);
|
|
562
562
|
}
|
|
563
563
|
const baseUrl = normalizeBaseUrl(model.baseUrl);
|
|
564
564
|
let body = createChatBody(model, context, options);
|
|
@@ -606,16 +606,19 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
606
606
|
}
|
|
607
607
|
if (!response.ok) {
|
|
608
608
|
capturedErrorResponse = await captureHttpErrorResponse(response);
|
|
609
|
-
throw new OllamaApiError(`HTTP ${response.status} from ${baseUrl}/api/chat`, response.status, {
|
|
609
|
+
throw new AIError.OllamaApiError(`HTTP ${response.status} from ${baseUrl}/api/chat`, response.status, {
|
|
610
610
|
headers: response.headers,
|
|
611
611
|
});
|
|
612
612
|
}
|
|
613
613
|
if (!response.body) {
|
|
614
|
-
throw new
|
|
614
|
+
throw new AIError.OllamaApiError("Ollama returned an empty response body", response.status, {
|
|
615
|
+
headers: response.headers,
|
|
616
|
+
});
|
|
615
617
|
}
|
|
616
618
|
stream.push({ type: "start", partial: output });
|
|
617
619
|
for await (const chunk of iterateNdjson(response.body)) {
|
|
618
620
|
if (chunk.message?.thinking) {
|
|
621
|
+
suppressHealedThinking = true;
|
|
619
622
|
endActiveTextBlock();
|
|
620
623
|
if (activeThinkingIndex === undefined) {
|
|
621
624
|
output.content.push({ type: "thinking", thinking: "" });
|
|
@@ -633,7 +636,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
633
636
|
});
|
|
634
637
|
}
|
|
635
638
|
if (!firstTokenTime) {
|
|
636
|
-
firstTokenTime =
|
|
639
|
+
firstTokenTime = performance.now();
|
|
637
640
|
}
|
|
638
641
|
}
|
|
639
642
|
const chunkContent = chunk.message?.content;
|
|
@@ -662,7 +665,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
662
665
|
id: `ollama:${output.content.length}:${name}`,
|
|
663
666
|
name,
|
|
664
667
|
arguments: parseStreamingJson<Record<string, unknown>>(partialJson),
|
|
665
|
-
partialJson,
|
|
668
|
+
[kStreamingPartialJson]: partialJson,
|
|
666
669
|
};
|
|
667
670
|
output.content.push(toolCall);
|
|
668
671
|
const index = output.content.length - 1;
|
|
@@ -675,7 +678,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
675
678
|
partial: output,
|
|
676
679
|
});
|
|
677
680
|
if (!firstTokenTime) {
|
|
678
|
-
firstTokenTime =
|
|
681
|
+
firstTokenTime = performance.now();
|
|
679
682
|
}
|
|
680
683
|
}
|
|
681
684
|
}
|
|
@@ -723,7 +726,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
723
726
|
if (output.stopReason === "stop" && output.content.some(block => block.type === "toolCall")) {
|
|
724
727
|
output.stopReason = "toolUse";
|
|
725
728
|
}
|
|
726
|
-
output.duration =
|
|
729
|
+
output.duration = performance.now() - startTime;
|
|
727
730
|
if (firstTokenTime) {
|
|
728
731
|
output.ttft = firstTokenTime - startTime;
|
|
729
732
|
}
|
|
@@ -739,13 +742,20 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
739
742
|
} catch (error) {
|
|
740
743
|
for (const block of output.content) {
|
|
741
744
|
if (block.type === "toolCall") {
|
|
742
|
-
|
|
745
|
+
clearStreamingPartialJson(block);
|
|
743
746
|
}
|
|
744
747
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
748
|
+
const result = await AIError.finalize(error, {
|
|
749
|
+
api: model.api,
|
|
750
|
+
signal: options.signal,
|
|
751
|
+
rawRequestDump,
|
|
752
|
+
capturedErrorResponse,
|
|
753
|
+
});
|
|
754
|
+
output.stopReason = result.stopReason;
|
|
755
|
+
output.errorStatus = result.status;
|
|
756
|
+
output.errorId = result.id;
|
|
757
|
+
output.errorMessage = result.message;
|
|
758
|
+
output.duration = performance.now() - startTime;
|
|
749
759
|
if (firstTokenTime) {
|
|
750
760
|
output.ttft = firstTokenTime - startTime;
|
|
751
761
|
}
|
|
@@ -6,6 +6,7 @@ import { resolvePromptCacheKey } from "../auth-gateway/http";
|
|
|
6
6
|
* `stream(model, context, options)`.
|
|
7
7
|
*/
|
|
8
8
|
import type { AuthGatewayStreamControl, AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
|
|
9
|
+
import * as AIError from "../error";
|
|
9
10
|
import type {
|
|
10
11
|
AssistantMessage,
|
|
11
12
|
AssistantMessageEventStream,
|
|
@@ -53,7 +54,7 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
|
|
|
53
54
|
// vendor-neutral headers when the body doesn't carry one.
|
|
54
55
|
const parsed = openaiChatRequestSchema(body);
|
|
55
56
|
if (parsed instanceof type.errors) {
|
|
56
|
-
throw new
|
|
57
|
+
throw new AIError.ValidationError(`openai-chat: ${parsed.summary}`);
|
|
57
58
|
}
|
|
58
59
|
const data = parsed;
|
|
59
60
|
|
|
@@ -14,11 +14,11 @@ export interface ReasoningConfig {
|
|
|
14
14
|
export interface CodexRequestOptions {
|
|
15
15
|
reasoningEffort?: ReasoningConfig["effort"];
|
|
16
16
|
reasoningSummary?: ReasoningConfig["summary"] | null;
|
|
17
|
-
/** Explicit `reasoning.context` override
|
|
17
|
+
/** Explicit `reasoning.context` override; defaults to `all_turns` for every Codex request when unset. */
|
|
18
18
|
reasoningContext?: CodexReasoningContext;
|
|
19
19
|
textVerbosity?: "low" | "medium" | "high";
|
|
20
20
|
include?: string[];
|
|
21
|
-
/** Responses Lite transport contract: strips image detail and
|
|
21
|
+
/** Responses Lite transport contract: strips image detail and disables parallel tool calling, mirroring codex-rs. */
|
|
22
22
|
responsesLite?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -195,8 +195,13 @@ function stripImageDetails(input: InputItem[]): void {
|
|
|
195
195
|
for (const collection of [item.content, item.output]) {
|
|
196
196
|
if (!Array.isArray(collection)) continue;
|
|
197
197
|
for (const part of collection) {
|
|
198
|
-
if (
|
|
199
|
-
|
|
198
|
+
if (
|
|
199
|
+
part &&
|
|
200
|
+
typeof part === "object" &&
|
|
201
|
+
(part as { type?: unknown }).type === "input_image" &&
|
|
202
|
+
"detail" in part
|
|
203
|
+
) {
|
|
204
|
+
part.detail = undefined;
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
}
|
|
@@ -249,20 +254,16 @@ export async function transformRequestBody(
|
|
|
249
254
|
...body.reasoning,
|
|
250
255
|
...reasoningConfig,
|
|
251
256
|
};
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
const reasoningContext = options.reasoningContext ?? (responsesLite ? "all_turns" : undefined);
|
|
256
|
-
if (reasoningContext !== undefined) {
|
|
257
|
-
body.reasoning.context = reasoningContext;
|
|
258
|
-
}
|
|
257
|
+
// Default reasoning replay to `all_turns` for every Codex request,
|
|
258
|
+
// mirroring codex-rs; an explicit `reasoningContext` overrides it.
|
|
259
|
+
body.reasoning.context = options.reasoningContext ?? "all_turns";
|
|
259
260
|
} else {
|
|
260
261
|
delete body.reasoning;
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
body.text = {
|
|
264
265
|
...body.text,
|
|
265
|
-
verbosity: options.textVerbosity || "
|
|
266
|
+
verbosity: options.textVerbosity || "high",
|
|
266
267
|
};
|
|
267
268
|
|
|
268
269
|
const include = Array.isArray(options.include) ? [...options.include] : [];
|