@oh-my-pi/pi-ai 17.2.4 → 17.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/types/dialect/examples.d.ts +18 -2
- package/dist/types/dialect/inventory.d.ts +6 -9
- package/dist/types/dialect/rendering.d.ts +9 -0
- package/dist/types/dialect/types.d.ts +0 -1
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/error/flags.d.ts +5 -0
- package/dist/types/providers/aws-credentials.d.ts +5 -10
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/openai-shared.d.ts +17 -5
- package/dist/types/providers/transform-messages.d.ts +1 -1
- package/dist/types/registry/amazon-bedrock.d.ts +7 -1
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/registry.d.ts +25 -1
- package/dist/types/registry/types.d.ts +24 -0
- package/dist/types/types.d.ts +11 -4
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/harmony-leak.d.ts +9 -0
- package/dist/types/utils/schema/typescript.d.ts +8 -2
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +2 -1
- package/src/auth-broker/wire-schema-resource.ts +6 -1
- package/src/auth-storage.ts +2 -2
- package/src/dialect/examples.ts +50 -12
- package/src/dialect/gemini.ts +17 -31
- package/src/dialect/harmony.ts +1 -2
- package/src/dialect/inventory.ts +21 -64
- package/src/dialect/rendering.ts +54 -0
- package/src/dialect/types.ts +0 -1
- package/src/error/aws.ts +5 -1
- package/src/error/flags.ts +8 -0
- package/src/providers/amazon-bedrock.ts +5 -5
- package/src/providers/anthropic.ts +77 -32
- package/src/providers/aws-credentials.ts +262 -76
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/cursor.ts +6 -3
- package/src/providers/ollama.ts +27 -1
- package/src/providers/openai-codex-responses.ts +19 -5
- package/src/providers/openai-shared.ts +84 -35
- package/src/providers/transform-messages.ts +1 -1
- package/src/registry/amazon-bedrock.ts +9 -14
- package/src/registry/aws.ts +57 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/google-vertex.ts +2 -2
- package/src/registry/registry.ts +2 -0
- package/src/registry/types.ts +30 -0
- package/src/stream.ts +46 -30
- package/src/types.ts +14 -8
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/harmony-leak.ts +12 -0
- package/src/utils/schema/typescript.ts +21 -7
- package/src/utils.ts +1 -1
package/src/stream.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
streamOpenAIResponses,
|
|
61
61
|
} from "./providers/register-builtins";
|
|
62
62
|
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
63
|
-
import { PROVIDER_REGISTRY } from "./registry";
|
|
63
|
+
import { getProviderDefinition, PROVIDER_REGISTRY } from "./registry";
|
|
64
64
|
import type {
|
|
65
65
|
Api,
|
|
66
66
|
AssistantMessage,
|
|
@@ -812,33 +812,37 @@ function streamDispatch<TApi extends Api>(
|
|
|
812
812
|
} as GitLabDuoWorkflowOptions);
|
|
813
813
|
}
|
|
814
814
|
|
|
815
|
-
// Vertex AI
|
|
815
|
+
// Vertex AI and Bedrock Converse authenticate outside the generic API-key path.
|
|
816
816
|
if (model.api === "google-vertex") {
|
|
817
817
|
return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
|
|
818
|
-
}
|
|
819
|
-
|
|
818
|
+
}
|
|
819
|
+
if (model.api === "bedrock-converse-stream") {
|
|
820
820
|
return streamBedrock(model as Model<"bedrock-converse-stream">, context, requestOptions as BedrockOptions);
|
|
821
821
|
}
|
|
822
822
|
|
|
823
|
-
const
|
|
823
|
+
const prepareRequest = getProviderDefinition(model.provider)?.prepareRequest;
|
|
824
|
+
const prepared = prepareRequest?.(model as Model<Api>, requestOptions as StreamOptions);
|
|
825
|
+
const providerModel = prepared?.model ?? (model as Model<Api>);
|
|
826
|
+
const preparedOptions = prepared?.options ?? (requestOptions as StreamOptions);
|
|
827
|
+
const apiKey = preparedOptions.apiKey || getEnvApiKey(providerModel.provider);
|
|
824
828
|
if (!apiKey) {
|
|
825
|
-
throw new AIError.MissingApiKeyError(
|
|
829
|
+
throw new AIError.MissingApiKeyError(providerModel.provider);
|
|
826
830
|
}
|
|
827
|
-
const providerOptions = isGoogleVertexAuthenticatedModel(
|
|
831
|
+
const providerOptions = isGoogleVertexAuthenticatedModel(providerModel)
|
|
828
832
|
? {
|
|
829
|
-
...
|
|
833
|
+
...preparedOptions,
|
|
830
834
|
apiKey: "vertex-adc",
|
|
831
|
-
fetch: createVertexAuthenticatedFetch(
|
|
835
|
+
fetch: createVertexAuthenticatedFetch(preparedOptions),
|
|
832
836
|
}
|
|
833
|
-
: { ...
|
|
837
|
+
: { ...preparedOptions, apiKey };
|
|
834
838
|
|
|
835
|
-
const api: Api =
|
|
839
|
+
const api: Api = providerModel.api;
|
|
836
840
|
switch (api) {
|
|
837
841
|
case "anthropic-messages": {
|
|
838
842
|
const anthropicOptions = providerOptions as AnthropicOptions;
|
|
839
|
-
return streamAnthropic(
|
|
843
|
+
return streamAnthropic(providerModel as Model<"anthropic-messages">, context, {
|
|
840
844
|
...anthropicOptions,
|
|
841
|
-
isOAuth: anthropicOptions.isOAuth ??
|
|
845
|
+
isOAuth: anthropicOptions.isOAuth ?? providerModel.isOAuth,
|
|
842
846
|
});
|
|
843
847
|
}
|
|
844
848
|
|
|
@@ -846,13 +850,13 @@ function streamDispatch<TApi extends Api>(
|
|
|
846
850
|
const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
|
|
847
851
|
if (useResponses) {
|
|
848
852
|
return streamOpenAIResponses(
|
|
849
|
-
|
|
853
|
+
providerModel as Model<"openai-responses">,
|
|
850
854
|
context,
|
|
851
855
|
providerOptions as OptionsForApi<"openai-responses">,
|
|
852
856
|
);
|
|
853
857
|
}
|
|
854
858
|
return streamOpenAICompletions(
|
|
855
|
-
|
|
859
|
+
providerModel as Model<"openai-completions">,
|
|
856
860
|
context,
|
|
857
861
|
providerOptions as OptionsForApi<"openai-completions">,
|
|
858
862
|
);
|
|
@@ -860,50 +864,50 @@ function streamDispatch<TApi extends Api>(
|
|
|
860
864
|
|
|
861
865
|
case "openai-completions":
|
|
862
866
|
return streamOpenAICompletions(
|
|
863
|
-
|
|
867
|
+
providerModel as Model<"openai-completions">,
|
|
864
868
|
context,
|
|
865
869
|
providerOptions as OptionsForApi<"openai-completions">,
|
|
866
870
|
);
|
|
867
871
|
|
|
868
872
|
case "openai-responses":
|
|
869
873
|
return streamOpenAIResponses(
|
|
870
|
-
|
|
874
|
+
providerModel as Model<"openai-responses">,
|
|
871
875
|
context,
|
|
872
876
|
providerOptions as OptionsForApi<"openai-responses">,
|
|
873
877
|
);
|
|
874
878
|
|
|
875
879
|
case "azure-openai-responses":
|
|
876
880
|
return streamAzureOpenAIResponses(
|
|
877
|
-
|
|
881
|
+
providerModel as Model<"azure-openai-responses">,
|
|
878
882
|
context,
|
|
879
883
|
providerOptions as OptionsForApi<"azure-openai-responses">,
|
|
880
884
|
);
|
|
881
885
|
|
|
882
886
|
case "openai-codex-responses":
|
|
883
887
|
return streamOpenAICodexResponses(
|
|
884
|
-
|
|
888
|
+
providerModel as Model<"openai-codex-responses">,
|
|
885
889
|
context,
|
|
886
890
|
providerOptions as OptionsForApi<"openai-codex-responses">,
|
|
887
891
|
);
|
|
888
892
|
|
|
889
893
|
case "google-generative-ai":
|
|
890
|
-
return streamGoogle(
|
|
894
|
+
return streamGoogle(providerModel as Model<"google-generative-ai">, context, providerOptions);
|
|
891
895
|
|
|
892
896
|
case "google-gemini-cli":
|
|
893
897
|
return streamGoogleGeminiCli(
|
|
894
|
-
|
|
898
|
+
providerModel as Model<"google-gemini-cli">,
|
|
895
899
|
context,
|
|
896
900
|
providerOptions as GoogleGeminiCliOptions,
|
|
897
901
|
);
|
|
898
902
|
|
|
899
903
|
case "ollama-chat":
|
|
900
|
-
return streamOllama(
|
|
904
|
+
return streamOllama(providerModel as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
|
|
901
905
|
|
|
902
906
|
case "cursor-agent":
|
|
903
|
-
return streamCursor(
|
|
907
|
+
return streamCursor(providerModel as Model<"cursor-agent">, context, providerOptions as CursorOptions);
|
|
904
908
|
|
|
905
909
|
case "devin-agent":
|
|
906
|
-
return streamDevin(
|
|
910
|
+
return streamDevin(providerModel as Model<"devin-agent">, context, providerOptions as DevinOptions);
|
|
907
911
|
|
|
908
912
|
default:
|
|
909
913
|
throw new AIError.ConfigurationError(`Unhandled API: ${api}`);
|
|
@@ -1028,12 +1032,10 @@ export function streamSimple<TApi extends Api>(
|
|
|
1028
1032
|
if (apiKeyResolver) {
|
|
1029
1033
|
const outer = new AssistantMessageEventStream();
|
|
1030
1034
|
const signal = requestOptions?.signal;
|
|
1031
|
-
// One inner attempt against a resolved
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
|
|
1035
|
-
// emitted directly.
|
|
1036
|
-
const runAttempt = async (apiKey: string): Promise<AuthRetryFailure | undefined> => {
|
|
1035
|
+
// One inner attempt against a resolved key, or against the Bedrock AWS
|
|
1036
|
+
// credential chain when its optional resolver has no stored bearer key.
|
|
1037
|
+
// Retryable auth failures are buffered until replay is safe.
|
|
1038
|
+
const runAttempt = async (apiKey?: string): Promise<AuthRetryFailure | undefined> => {
|
|
1037
1039
|
const bufferedEvents: AssistantMessageEvent[] = [];
|
|
1038
1040
|
let emittedReplayUnsafeEvent = false;
|
|
1039
1041
|
const flushBuffered = (): void => {
|
|
@@ -1107,6 +1109,11 @@ export function streamSimple<TApi extends Api>(
|
|
|
1107
1109
|
return;
|
|
1108
1110
|
}
|
|
1109
1111
|
if (lastKey === undefined) {
|
|
1112
|
+
if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1113
|
+
const failure = await runAttempt();
|
|
1114
|
+
if (failure) emitFailure(failure);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1110
1117
|
outer.fail(new AIError.MissingApiKeyError(model.provider));
|
|
1111
1118
|
return;
|
|
1112
1119
|
}
|
|
@@ -1156,6 +1163,13 @@ export function streamSimple<TApi extends Api>(
|
|
|
1156
1163
|
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
1157
1164
|
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
1158
1165
|
return stream(model, context, providerOptions);
|
|
1166
|
+
} else if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1167
|
+
const providerOptions = mapOptionsForApi(
|
|
1168
|
+
model,
|
|
1169
|
+
requestOptions,
|
|
1170
|
+
typeof requestOptions.apiKey === "string" ? requestOptions.apiKey : getEnvApiKey(model.provider),
|
|
1171
|
+
);
|
|
1172
|
+
return stream(model, context, providerOptions);
|
|
1159
1173
|
}
|
|
1160
1174
|
|
|
1161
1175
|
// The resolver form is handled by the wrapper above; only a static string
|
|
@@ -1454,6 +1468,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1454
1468
|
apiKey?: string,
|
|
1455
1469
|
): OptionsForApi<TApi> {
|
|
1456
1470
|
const options = normalizeMandatoryReasoningOptions(model, rawOptions);
|
|
1471
|
+
const simpleProviderOptions = getProviderDefinition(model.provider)?.mapSimpleOptions?.(options ?? {});
|
|
1457
1472
|
const base = {
|
|
1458
1473
|
temperature: options?.temperature,
|
|
1459
1474
|
topP: options?.topP,
|
|
@@ -1483,6 +1498,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1483
1498
|
execHandlers: options?.execHandlers,
|
|
1484
1499
|
fetch: options?.fetch,
|
|
1485
1500
|
fallbacks: options?.fallbacks,
|
|
1501
|
+
...simpleProviderOptions,
|
|
1486
1502
|
};
|
|
1487
1503
|
|
|
1488
1504
|
switch (model.api) {
|
package/src/types.ts
CHANGED
|
@@ -214,10 +214,10 @@ export function resolveModelServiceTier(
|
|
|
214
214
|
|
|
215
215
|
/**
|
|
216
216
|
* True when the tier should be sent on the wire as the provider's service-tier
|
|
217
|
-
* request field. OpenAI / OpenAI-Codex accept
|
|
218
|
-
* (Gemini API + Vertex) and OpenRouter accept `flex`/`priority`;
|
|
219
|
-
* Serverless realizes only its Priority serving path. Anthropic is
|
|
220
|
-
* realizes `priority` via `speed: "fast"
|
|
217
|
+
* request field. OpenAI / OpenAI-Codex accept every {@link ServiceTier};
|
|
218
|
+
* Google (Gemini API + Vertex) and OpenRouter accept `flex`/`priority`;
|
|
219
|
+
* Fireworks Serverless realizes only its Priority serving path. Anthropic is
|
|
220
|
+
* absent because it realizes `priority` via `speed: "fast"`.
|
|
221
221
|
*/
|
|
222
222
|
export function shouldSendServiceTier(
|
|
223
223
|
serviceTier: ServiceTier | null | undefined,
|
|
@@ -225,12 +225,11 @@ export function shouldSendServiceTier(
|
|
|
225
225
|
): boolean {
|
|
226
226
|
if (!serviceTier) return false;
|
|
227
227
|
const provider = typeof target === "string" ? target : target?.provider;
|
|
228
|
-
if (provider === "openai" || provider === "openai-codex"
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (typeof target !== "string" && target && isOpenAIServiceTierModel(target)) {
|
|
228
|
+
if (provider === "openai" || provider === "openai-codex") return true;
|
|
229
|
+
if (provider === "openrouter") {
|
|
232
230
|
return serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority";
|
|
233
231
|
}
|
|
232
|
+
if (typeof target !== "string" && target && isOpenAIServiceTierModel(target)) return true;
|
|
234
233
|
if (provider === "google") {
|
|
235
234
|
return serviceTier === "flex" || serviceTier === "priority";
|
|
236
235
|
}
|
|
@@ -434,6 +433,11 @@ export interface StreamOptions {
|
|
|
434
433
|
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
|
435
434
|
*/
|
|
436
435
|
metadata?: Record<string, unknown>;
|
|
436
|
+
/**
|
|
437
|
+
* Provider-owned request configuration. Provider hooks interpret this bag;
|
|
438
|
+
* generic API transports do not forward its fields onto the wire.
|
|
439
|
+
*/
|
|
440
|
+
providerOptions?: Readonly<Record<string, unknown>>;
|
|
437
441
|
/** OpenAI Responses/Codex response fields to include verbatim. */
|
|
438
442
|
include?: OpenAIResponseInclude[];
|
|
439
443
|
/**
|
|
@@ -844,6 +848,8 @@ export interface AssistantRetryRecovery {
|
|
|
844
848
|
export interface ContextSnapshot {
|
|
845
849
|
promptTokens: number; // authoritative provider prompt/input tokens
|
|
846
850
|
nonMessageTokens: number; // estimated non-message total at send time
|
|
851
|
+
/** Estimated prompt tokens removed by local history rewrites after this provider snapshot was recorded. */
|
|
852
|
+
historyRewriteTokensRemoved?: number;
|
|
847
853
|
lastMessageTimestamp?: number;
|
|
848
854
|
}
|
|
849
855
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
|
+
|
|
6
|
+
/** INI sections with `profile ` / `sso-session ` prefixes normalized. */
|
|
7
|
+
export type AwsIniFile = Record<string, Record<string, string>>;
|
|
8
|
+
|
|
9
|
+
export function parseAwsIni(text: string): AwsIniFile {
|
|
10
|
+
const out: AwsIniFile = {};
|
|
11
|
+
let current: Record<string, string> | null = null;
|
|
12
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
13
|
+
const line = rawLine.trim();
|
|
14
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
15
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
16
|
+
let name = line.slice(1, -1).trim();
|
|
17
|
+
if (name.startsWith("profile ")) name = name.slice(8).trim();
|
|
18
|
+
if (name.startsWith("sso-session ")) name = `sso-session:${name.slice(12).trim()}`;
|
|
19
|
+
let section = out[name];
|
|
20
|
+
if (!section) {
|
|
21
|
+
section = {};
|
|
22
|
+
out[name] = section;
|
|
23
|
+
}
|
|
24
|
+
current = section;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!current) continue;
|
|
28
|
+
const eq = line.indexOf("=");
|
|
29
|
+
if (eq === -1) continue;
|
|
30
|
+
current[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readAwsIniSync(filePath: string): AwsIniFile | undefined {
|
|
36
|
+
try {
|
|
37
|
+
return parseAwsIni(fs.readFileSync(filePath, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve the selected shared-credentials profile. */
|
|
44
|
+
export function resolveAwsProfile(profile?: string): string {
|
|
45
|
+
return profile || $env.AWS_PROFILE || "default";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the shared config file participates in profile/region resolution.
|
|
50
|
+
* Explicit profile selection enables it; the implicit default profile follows
|
|
51
|
+
* the AWS SDK's `AWS_SDK_LOAD_CONFIG` opt-in.
|
|
52
|
+
*/
|
|
53
|
+
export function shouldLoadAwsSharedConfig(profile?: string): boolean {
|
|
54
|
+
if (profile || $env.AWS_PROFILE) return true;
|
|
55
|
+
const value = $env.AWS_SDK_LOAD_CONFIG?.toLowerCase();
|
|
56
|
+
return value === "1" || value === "true";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveAwsProfileRegion(profile?: string): string | undefined {
|
|
60
|
+
if (!shouldLoadAwsSharedConfig(profile)) return undefined;
|
|
61
|
+
const configPath = $env.AWS_CONFIG_FILE || path.join(os.homedir(), ".aws", "config");
|
|
62
|
+
return readAwsIniSync(configPath)?.[resolveAwsProfile(profile)]?.region;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Region selected by the environment or active shared-config profile. */
|
|
66
|
+
export function resolveAwsAmbientRegion(profile?: string): string | undefined {
|
|
67
|
+
return $env.AWS_REGION || $env.AWS_DEFAULT_REGION || resolveAwsProfileRegion(profile);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve the region precedence shared by AWS transports and credential exchanges. */
|
|
71
|
+
export function resolveAwsRegion(explicitRegion?: string, profile?: string): string {
|
|
72
|
+
return explicitRegion || resolveAwsAmbientRegion(profile) || "us-east-1";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function hasConfiguredAwsProfile(profile?: string): boolean {
|
|
76
|
+
const selectedProfile = resolveAwsProfile(profile);
|
|
77
|
+
const credentialsPath = $env.AWS_SHARED_CREDENTIALS_FILE || path.join(os.homedir(), ".aws", "credentials");
|
|
78
|
+
const configPath = $env.AWS_CONFIG_FILE || path.join(os.homedir(), ".aws", "config");
|
|
79
|
+
const credentialsIni = readAwsIniSync(credentialsPath);
|
|
80
|
+
const configIni = shouldLoadAwsSharedConfig(profile) ? readAwsIniSync(configPath) : undefined;
|
|
81
|
+
const merged = { ...(configIni?.[selectedProfile] ?? {}), ...(credentialsIni?.[selectedProfile] ?? {}) };
|
|
82
|
+
if (merged.aws_access_key_id && merged.aws_secret_access_key) return true;
|
|
83
|
+
if (merged.credential_process) return true;
|
|
84
|
+
if (!merged.sso_account_id || !merged.sso_role_name) return false;
|
|
85
|
+
if (merged.sso_start_url && merged.sso_region) return true;
|
|
86
|
+
const session = merged.sso_session ? configIni?.[`sso-session:${merged.sso_session}`] : undefined;
|
|
87
|
+
return !!(session?.sso_start_url && session.sso_region);
|
|
88
|
+
}
|
|
@@ -35,6 +35,18 @@ export function escapeHarmonyControlTokens(text: string): string {
|
|
|
35
35
|
return text.replace(HARMONY_CONTROL_TOKEN_ESCAPE_RE, "<\\|$1\\|>");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Escape reserved Harmony control tokens inside a JSON document string (e.g.
|
|
40
|
+
* `function_call.arguments`). Doubles the backslash so the document remains
|
|
41
|
+
* valid JSON whose *decoded* strings carry the inert `<\|token\|>` spelling.
|
|
42
|
+
* `<|` cannot occur outside a string literal in valid JSON, so the blanket
|
|
43
|
+
* replace never corrupts structure; malformed documents are escaped
|
|
44
|
+
* best-effort.
|
|
45
|
+
*/
|
|
46
|
+
export function escapeHarmonyControlTokensInJson(text: string): string {
|
|
47
|
+
return text.replace(HARMONY_CONTROL_TOKEN_ESCAPE_RE, "<\\\\|$1\\\\|>");
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
/**
|
|
39
51
|
* Whether requests to `model` are served by a Harmony-dialect backend
|
|
40
52
|
* (gpt-5.x / gpt-oss), which rejects reserved control-token spellings appearing
|
|
@@ -12,15 +12,22 @@
|
|
|
12
12
|
import { isJsonObject } from "./types";
|
|
13
13
|
|
|
14
14
|
export interface JsonSchemaToTsOptions {
|
|
15
|
-
/** Indentation unit for nested object bodies. Default two spaces. */
|
|
15
|
+
/** Indentation unit for nested object bodies. Default two spaces (none in `harmony` style). */
|
|
16
16
|
readonly indent?: string;
|
|
17
|
-
/** Emit `description` keywords as
|
|
17
|
+
/** Emit `description` keywords as comments on object properties. Default true. */
|
|
18
18
|
readonly comments?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Output flavor. `default` renders JSDoc comments, `;` delimiters, and
|
|
21
|
+
* indented bodies; `harmony` renders the flat OpenAI-Harmony convention —
|
|
22
|
+
* `//` line comments, `,` delimiters, no indentation.
|
|
23
|
+
*/
|
|
24
|
+
readonly style?: "default" | "harmony";
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
interface Ctx {
|
|
22
28
|
readonly indent: string;
|
|
23
29
|
readonly comments: boolean;
|
|
30
|
+
readonly harmony: boolean;
|
|
24
31
|
readonly defs: Record<string, unknown> | undefined;
|
|
25
32
|
readonly seen: Set<unknown>;
|
|
26
33
|
}
|
|
@@ -48,7 +55,11 @@ function joinUnion(parts: readonly string[]): string {
|
|
|
48
55
|
return unique.length > 0 ? unique.join(" | ") : "never";
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
function
|
|
58
|
+
function emitDescription(lines: string[], description: string, ctx: Ctx, pad: string): void {
|
|
59
|
+
if (ctx.harmony) {
|
|
60
|
+
for (const line of description.split("\n")) lines.push(`${pad}// ${line}`.trimEnd());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
52
63
|
// `* /` keeps a stray closing token inside the description from ending the comment.
|
|
53
64
|
const safe = description.replace(/\*\//g, "* /");
|
|
54
65
|
if (!safe.includes("\n")) {
|
|
@@ -85,6 +96,7 @@ function convertObject(node: Record<string, unknown>, ctx: Ctx, pad: string): st
|
|
|
85
96
|
const required = new Set(
|
|
86
97
|
Array.isArray(node.required) ? node.required.filter((key): key is string => typeof key === "string") : [],
|
|
87
98
|
);
|
|
99
|
+
const delimiter = ctx.harmony ? "," : ";";
|
|
88
100
|
for (const key in properties) {
|
|
89
101
|
const value = properties[key];
|
|
90
102
|
if (
|
|
@@ -93,11 +105,11 @@ function convertObject(node: Record<string, unknown>, ctx: Ctx, pad: string): st
|
|
|
93
105
|
typeof value.description === "string" &&
|
|
94
106
|
value.description.length > 0
|
|
95
107
|
) {
|
|
96
|
-
|
|
108
|
+
emitDescription(body, value.description, ctx, childPad);
|
|
97
109
|
}
|
|
98
110
|
const optional = required.has(key) ? "" : "?";
|
|
99
111
|
const name = SAFE_KEY.test(key) ? key : JSON.stringify(key);
|
|
100
|
-
body.push(`${childPad}${name}${optional}: ${convert(value, ctx, childPad)}
|
|
112
|
+
body.push(`${childPad}${name}${optional}: ${convert(value, ctx, childPad)}${delimiter}`);
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -110,7 +122,7 @@ function convertObject(node: Record<string, unknown>, ctx: Ctx, pad: string): st
|
|
|
110
122
|
|
|
111
123
|
// Named properties alongside a free-form value schema → index signature.
|
|
112
124
|
if (isJsonObject(additional)) {
|
|
113
|
-
body.push(`${childPad}[key: string]: ${convert(additional, ctx, childPad)}
|
|
125
|
+
body.push(`${childPad}[key: string]: ${convert(additional, ctx, childPad)}${ctx.harmony ? "," : ";"}`);
|
|
114
126
|
}
|
|
115
127
|
return `{\n${body.join("\n")}\n${pad}}`;
|
|
116
128
|
}
|
|
@@ -188,9 +200,11 @@ export function jsonSchemaToTypeScript(schema: unknown, options?: JsonSchemaToTs
|
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
202
|
}
|
|
203
|
+
const harmony = options?.style === "harmony";
|
|
191
204
|
const ctx: Ctx = {
|
|
192
|
-
indent: options?.indent ?? " ",
|
|
205
|
+
indent: options?.indent ?? (harmony ? "" : " "),
|
|
193
206
|
comments: options?.comments ?? true,
|
|
207
|
+
harmony,
|
|
194
208
|
defs,
|
|
195
209
|
seen: new Set(),
|
|
196
210
|
};
|
package/src/utils.ts
CHANGED
|
@@ -403,7 +403,7 @@ function sanitizeOpenAIResponsesReasoningItemForReplay(
|
|
|
403
403
|
function sanitizeOpenAIResponsesImageGenerationCallForReplay(
|
|
404
404
|
item: Record<string, unknown>,
|
|
405
405
|
): ResponseInputItem.ImageGenerationCall | undefined {
|
|
406
|
-
if (typeof item.id !== "string" || item.
|
|
406
|
+
if (typeof item.id !== "string" || typeof item.result !== "string" || item.result.length === 0) {
|
|
407
407
|
return undefined;
|
|
408
408
|
}
|
|
409
409
|
return {
|