@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/types/dialect/examples.d.ts +18 -2
  3. package/dist/types/dialect/inventory.d.ts +6 -9
  4. package/dist/types/dialect/rendering.d.ts +9 -0
  5. package/dist/types/dialect/types.d.ts +0 -1
  6. package/dist/types/error/aws.d.ts +5 -1
  7. package/dist/types/error/flags.d.ts +5 -0
  8. package/dist/types/providers/aws-credentials.d.ts +5 -10
  9. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  10. package/dist/types/providers/openai-shared.d.ts +17 -5
  11. package/dist/types/providers/transform-messages.d.ts +1 -1
  12. package/dist/types/registry/amazon-bedrock.d.ts +7 -1
  13. package/dist/types/registry/aws.d.ts +13 -0
  14. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  15. package/dist/types/registry/registry.d.ts +25 -1
  16. package/dist/types/registry/types.d.ts +24 -0
  17. package/dist/types/types.d.ts +11 -4
  18. package/dist/types/utils/aws-profile.d.ts +17 -0
  19. package/dist/types/utils/harmony-leak.d.ts +9 -0
  20. package/dist/types/utils/schema/typescript.d.ts +8 -2
  21. package/package.json +4 -4
  22. package/src/auth-broker/discover.ts +2 -1
  23. package/src/auth-broker/wire-schema-resource.ts +6 -1
  24. package/src/auth-storage.ts +2 -2
  25. package/src/dialect/examples.ts +50 -12
  26. package/src/dialect/gemini.ts +17 -31
  27. package/src/dialect/harmony.ts +1 -2
  28. package/src/dialect/inventory.ts +21 -64
  29. package/src/dialect/rendering.ts +54 -0
  30. package/src/dialect/types.ts +0 -1
  31. package/src/error/aws.ts +5 -1
  32. package/src/error/flags.ts +8 -0
  33. package/src/providers/amazon-bedrock.ts +5 -5
  34. package/src/providers/anthropic.ts +77 -32
  35. package/src/providers/aws-credentials.ts +262 -76
  36. package/src/providers/bedrock-mantle.ts +110 -0
  37. package/src/providers/cursor.ts +6 -3
  38. package/src/providers/ollama.ts +27 -1
  39. package/src/providers/openai-codex-responses.ts +19 -5
  40. package/src/providers/openai-shared.ts +84 -35
  41. package/src/providers/transform-messages.ts +1 -1
  42. package/src/registry/amazon-bedrock.ts +9 -14
  43. package/src/registry/aws.ts +57 -0
  44. package/src/registry/bedrock-mantle.ts +34 -0
  45. package/src/registry/google-vertex.ts +2 -2
  46. package/src/registry/registry.ts +2 -0
  47. package/src/registry/types.ts +30 -0
  48. package/src/stream.ts +46 -30
  49. package/src/types.ts +14 -8
  50. package/src/utils/aws-profile.ts +88 -0
  51. package/src/utils/harmony-leak.ts +12 -0
  52. package/src/utils/schema/typescript.ts +21 -7
  53. 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 uses Application Default Credentials, not API keys
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
- } else if (model.api === "bedrock-converse-stream") {
819
- // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
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 apiKey = requestOptions.apiKey || getEnvApiKey(model.provider);
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(model.provider);
829
+ throw new AIError.MissingApiKeyError(providerModel.provider);
826
830
  }
827
- const providerOptions = isGoogleVertexAuthenticatedModel(model)
831
+ const providerOptions = isGoogleVertexAuthenticatedModel(providerModel)
828
832
  ? {
829
- ...requestOptions,
833
+ ...preparedOptions,
830
834
  apiKey: "vertex-adc",
831
- fetch: createVertexAuthenticatedFetch(requestOptions),
835
+ fetch: createVertexAuthenticatedFetch(preparedOptions),
832
836
  }
833
- : { ...requestOptions, apiKey };
837
+ : { ...preparedOptions, apiKey };
834
838
 
835
- const api: Api = model.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(model as Model<"anthropic-messages">, context, {
843
+ return streamAnthropic(providerModel as Model<"anthropic-messages">, context, {
840
844
  ...anthropicOptions,
841
- isOAuth: anthropicOptions.isOAuth ?? model.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
- model as Model<"openai-responses">,
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
- model as Model<"openai-completions">,
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
- model as Model<"openai-completions">,
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
- model as Model<"openai-responses">,
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
- model as Model<"azure-openai-responses">,
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
- model as Model<"openai-codex-responses">,
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(model as Model<"google-generative-ai">, context, providerOptions);
894
+ return streamGoogle(providerModel as Model<"google-generative-ai">, context, providerOptions);
891
895
 
892
896
  case "google-gemini-cli":
893
897
  return streamGoogleGeminiCli(
894
- model as Model<"google-gemini-cli">,
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(model as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
904
+ return streamOllama(providerModel as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
901
905
 
902
906
  case "cursor-agent":
903
- return streamCursor(model as Model<"cursor-agent">, context, providerOptions as CursorOptions);
907
+ return streamCursor(providerModel as Model<"cursor-agent">, context, providerOptions as CursorOptions);
904
908
 
905
909
  case "devin-agent":
906
- return streamDevin(model as Model<"devin-agent">, context, providerOptions as DevinOptions);
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 string key. A retryable auth error
1032
- // that arrives before any replay-unsafe event is buffered and returned
1033
- // (so the caller can retry with a fresh key) instead of surfaced. Once any
1034
- // non-start event escapes, retry is no longer safe and the failure is
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 `flex`/`scale`/`priority`; Google
218
- * (Gemini API + Vertex) and OpenRouter accept `flex`/`priority`; Fireworks
219
- * Serverless realizes only its Priority serving path. Anthropic is absent — it
220
- * realizes `priority` via `speed: "fast"`, not a service-tier field.
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" || provider === "openrouter") {
229
- return serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority";
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 JSDoc comments on object properties. Default true. */
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 emitJsDoc(lines: string[], description: string, pad: string): void {
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
- emitJsDoc(body, value.description, childPad);
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.status !== "completed" || typeof item.result !== "string") {
406
+ if (typeof item.id !== "string" || typeof item.result !== "string" || item.result.length === 0) {
407
407
  return undefined;
408
408
  }
409
409
  return {