@gajae-code/ai 0.2.0 → 0.2.1

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.1] - 2026-05-30
6
+
7
+ ### Changed
8
+
9
+ - Refreshed AI package metadata for the GJC 0.2.1 release.
10
+
5
11
  ## [0.2.0] - 2026-05-28
6
12
 
7
13
  ### Fixed
@@ -0,0 +1,4 @@
1
+ import type { FetchImpl, ModelRequestTransform } from "../types";
2
+ export declare function applyOpenAIRequestTransformHeaders(headers: Record<string, string>, transform: ModelRequestTransform | undefined, profileUserAgent: string): Record<string, string>;
3
+ export declare function applyOpenAIRequestTransformBody(params: object, transform: ModelRequestTransform | undefined): void;
4
+ export declare function wrapFetchForOpenAIRequestTransform(baseFetch: FetchImpl, transform: ModelRequestTransform | undefined, profileUserAgent: string): FetchImpl;
@@ -689,6 +689,16 @@ export interface VercelGatewayRouting {
689
689
  /** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */
690
690
  order?: string[];
691
691
  }
692
+ export interface ModelRequestTransform {
693
+ /** Named request-shaping preset. `openai-proxy` removes OpenAI SDK telemetry headers and uses a generic Gajae-Code User-Agent. */
694
+ profile?: "openai-proxy";
695
+ /** Header names to remove from the final outbound request. Case-insensitive. */
696
+ stripHeaders?: string[];
697
+ /** Headers to set after stripping; use null to remove a header explicitly. */
698
+ setHeaders?: Record<string, string | null>;
699
+ /** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
700
+ extraBody?: Record<string, unknown>;
701
+ }
692
702
  export interface Model<TApi extends Api = any> {
693
703
  id: string;
694
704
  name: string;
@@ -727,6 +737,10 @@ export interface Model<TApi extends Api = any> {
727
737
  preferWebsockets?: boolean;
728
738
  /** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
729
739
  contextPromotionTarget?: string;
740
+ /** Provider-facing model id when it differs from the local selector id. */
741
+ wireModelId?: string;
742
+ /** Declarative request shaping for OpenAI-compatible proxy providers. */
743
+ requestTransform?: ModelRequestTransform;
730
744
  /** Provider-assigned priority value (lower = higher priority). */
731
745
  priority?: number;
732
746
  /** Canonical thinking capability metadata for this model. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.2.0",
46
+ "@gajae-code/utils": "0.2.1",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -4,7 +4,7 @@
4
4
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
5
5
  */
6
6
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
7
- const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.35.3";
7
+ const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.44.1";
8
8
  const platform = process.platform === "win32" ? "win32" : process.platform;
9
9
  const arch = process.arch === "x64" ? "x64" : process.arch;
10
10
  return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
@@ -66,6 +66,11 @@ import {
66
66
  resolveGitHubCopilotBaseUrl,
67
67
  } from "./github-copilot-headers";
68
68
  import { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "./openai-completions-compat";
69
+ import {
70
+ applyOpenAIRequestTransformBody,
71
+ applyOpenAIRequestTransformHeaders,
72
+ wrapFetchForOpenAIRequestTransform,
73
+ } from "./openai-request-transform";
69
74
  import { createInitialResponsesAssistantMessage } from "./openai-responses-shared";
70
75
  import { transformMessages } from "./transform-messages";
71
76
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
@@ -956,6 +961,7 @@ async function createClient(
956
961
  if (model.provider === "kimi-code") {
957
962
  headers = { ...getKimiCommonHeaders(), ...headers };
958
963
  }
964
+ headers = applyOpenAIRequestTransformHeaders(headers, model.requestTransform, `Gajae-Code/${packageJson.version}`);
959
965
  let copilotPremiumRequests: number | undefined;
960
966
 
961
967
  let baseUrl =
@@ -1013,7 +1019,14 @@ async function createClient(
1013
1019
  },
1014
1020
  baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
1015
1021
  );
1016
- const debugFetch = onSseEvent ? wrapFetchForSseDebug(wrappedFetch, event => onSseEvent(event, model)) : wrappedFetch;
1022
+ const transformedFetch = wrapFetchForOpenAIRequestTransform(
1023
+ wrappedFetch,
1024
+ model.requestTransform,
1025
+ `Gajae-Code/${packageJson.version}`,
1026
+ );
1027
+ const debugFetch = onSseEvent
1028
+ ? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
1029
+ : transformedFetch;
1017
1030
  // Bound HTTP request timeout to roughly the first-event watchdog window.
1018
1031
  // The OpenAI SDK's default is 10 minutes per attempt × `maxRetries`, which
1019
1032
  // turns a stalled-before-headers fetch into a multi-minute hang invisible
@@ -1078,11 +1091,12 @@ function buildParams(
1078
1091
  const effectiveMaxTokens = options?.maxTokens ?? (isKimi ? model.maxTokens : undefined);
1079
1092
 
1080
1093
  const requestModelId =
1081
- model.provider === "fireworks"
1094
+ model.wireModelId ??
1095
+ (model.provider === "fireworks"
1082
1096
  ? toFireworksWireModelId(model.id)
1083
1097
  : model.provider === "firepass"
1084
1098
  ? toFirepassWireModelId(model.id)
1085
- : model.id;
1099
+ : model.id);
1086
1100
  const params: OpenAICompletionsParams = {
1087
1101
  model: requestModelId,
1088
1102
  messages,
@@ -1260,6 +1274,7 @@ function buildParams(
1260
1274
  if (compat.extraBody) {
1261
1275
  Object.assign(params, compat.extraBody);
1262
1276
  }
1277
+ applyOpenAIRequestTransformBody(params, model.requestTransform);
1263
1278
 
1264
1279
  return { params, toolStrictMode };
1265
1280
  }
@@ -0,0 +1,135 @@
1
+ import type { FetchImpl, ModelRequestTransform } from "../types";
2
+
3
+ const PROTECTED_EXTRA_BODY_KEYS = new Set([
4
+ "model",
5
+ "messages",
6
+ "input",
7
+ "instructions",
8
+ "stream",
9
+ "stream_options",
10
+ "store",
11
+ "max_tokens",
12
+ "max_completion_tokens",
13
+ "max_output_tokens",
14
+ "temperature",
15
+ "top_p",
16
+ "presence_penalty",
17
+ "frequency_penalty",
18
+ "reasoning",
19
+ "reasoning_effort",
20
+ "prompt_cache_key",
21
+ "prompt_cache_retention",
22
+ "service_tier",
23
+ "stop",
24
+ "tools",
25
+ "tool_choice",
26
+ "parallel_tool_calls",
27
+ ]);
28
+
29
+ const OPENAI_PROXY_STRIP_HEADERS = [
30
+ "x-stainless-arch",
31
+ "x-stainless-async",
32
+ "x-stainless-lang",
33
+ "x-stainless-os",
34
+ "x-stainless-package-version",
35
+ "x-stainless-retry-count",
36
+ "x-stainless-runtime",
37
+ "x-stainless-runtime-version",
38
+ "x-stainless-timeout",
39
+ "x-stainless-helper-method",
40
+ "openai-organization",
41
+ "openai-project",
42
+ ] as const;
43
+
44
+ function resolveRequestTransform(
45
+ transform: ModelRequestTransform | undefined,
46
+ profileUserAgent: string,
47
+ ): ModelRequestTransform | undefined {
48
+ if (!transform) return undefined;
49
+ const profileTransform: ModelRequestTransform =
50
+ transform.profile === "openai-proxy"
51
+ ? {
52
+ stripHeaders: [...OPENAI_PROXY_STRIP_HEADERS],
53
+ setHeaders: { "User-Agent": profileUserAgent },
54
+ }
55
+ : {};
56
+ return {
57
+ ...profileTransform,
58
+ ...transform,
59
+ stripHeaders: transform.stripHeaders ?? profileTransform.stripHeaders,
60
+ setHeaders: transform.setHeaders
61
+ ? { ...(profileTransform.setHeaders ?? {}), ...transform.setHeaders }
62
+ : profileTransform.setHeaders,
63
+ extraBody: transform.extraBody,
64
+ };
65
+ }
66
+
67
+ function deleteHeaders(headers: Headers, names: readonly string[] | undefined): void {
68
+ for (const name of names ?? []) {
69
+ headers.delete(name);
70
+ }
71
+ }
72
+
73
+ function setHeaders(headers: Headers, values: Record<string, string | null> | undefined): void {
74
+ for (const [name, value] of Object.entries(values ?? {})) {
75
+ if (value === null) {
76
+ headers.delete(name);
77
+ } else {
78
+ headers.set(name, value);
79
+ }
80
+ }
81
+ }
82
+
83
+ function transformHeaders(
84
+ headers: RequestInit["headers"] | undefined,
85
+ transform: ModelRequestTransform | undefined,
86
+ ): Headers {
87
+ const result = new Headers(headers);
88
+ deleteHeaders(result, transform?.stripHeaders);
89
+ setHeaders(result, transform?.setHeaders);
90
+ return result;
91
+ }
92
+
93
+ export function applyOpenAIRequestTransformHeaders(
94
+ headers: Record<string, string>,
95
+ transform: ModelRequestTransform | undefined,
96
+ profileUserAgent: string,
97
+ ): Record<string, string> {
98
+ const resolved = resolveRequestTransform(transform, profileUserAgent);
99
+ if (!resolved) return headers;
100
+ return Object.fromEntries(transformHeaders(headers, resolved).entries());
101
+ }
102
+
103
+ export function applyOpenAIRequestTransformBody(params: object, transform: ModelRequestTransform | undefined): void {
104
+ if (!transform?.extraBody) return;
105
+ const body = params as Record<string, unknown>;
106
+ for (const [key, value] of Object.entries(transform.extraBody)) {
107
+ if (!PROTECTED_EXTRA_BODY_KEYS.has(key) && !(key in body)) {
108
+ body[key] = value;
109
+ }
110
+ }
111
+ }
112
+
113
+ export function wrapFetchForOpenAIRequestTransform(
114
+ baseFetch: FetchImpl,
115
+ transform: ModelRequestTransform | undefined,
116
+ profileUserAgent: string,
117
+ ): FetchImpl {
118
+ const resolved = resolveRequestTransform(transform, profileUserAgent);
119
+ if (!resolved) return baseFetch;
120
+ return Object.assign(
121
+ async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
122
+ if (input instanceof Request) {
123
+ const request = new Request(input, init);
124
+ deleteHeaders(request.headers, resolved.stripHeaders);
125
+ setHeaders(request.headers, resolved.setHeaders);
126
+ return baseFetch(request);
127
+ }
128
+ return baseFetch(input, {
129
+ ...init,
130
+ headers: transformHeaders(init?.headers, resolved),
131
+ });
132
+ },
133
+ baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
134
+ );
135
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  ResponseCreateParamsStreaming,
6
6
  ResponseInput,
7
7
  } from "openai/resources/responses/responses";
8
+ import packageJson from "../../package.json" with { type: "json" };
8
9
  import { getEnvApiKey } from "../stream";
9
10
  import type {
10
11
  AssistantMessage,
@@ -50,6 +51,11 @@ import {
50
51
  resolveGitHubCopilotBaseUrl,
51
52
  } from "./github-copilot-headers";
52
53
  import { compactGrammarDefinition } from "./grammar";
54
+ import {
55
+ applyOpenAIRequestTransformBody,
56
+ applyOpenAIRequestTransformHeaders,
57
+ wrapFetchForOpenAIRequestTransform,
58
+ } from "./openai-request-transform";
53
59
  import {
54
60
  appendResponsesToolResultMessages,
55
61
  applyCommonResponsesSamplingParams,
@@ -363,7 +369,11 @@ function createClient(
363
369
  }
364
370
  const rawApiKey = apiKey;
365
371
 
366
- const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) };
372
+ const headers = applyOpenAIRequestTransformHeaders(
373
+ { ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
374
+ model.requestTransform,
375
+ `Gajae-Code/${packageJson.version}`,
376
+ );
367
377
  let copilotPremiumRequests: number | undefined;
368
378
 
369
379
  let baseUrl =
@@ -390,6 +400,11 @@ function createClient(
390
400
  headers["x-client-request-id"] ??= sessionId;
391
401
  }
392
402
  const baseFetch = fetchOverride ?? fetch;
403
+ const transformedFetch = wrapFetchForOpenAIRequestTransform(
404
+ baseFetch,
405
+ model.requestTransform,
406
+ `Gajae-Code/${packageJson.version}`,
407
+ );
393
408
  return {
394
409
  client: new OpenAI({
395
410
  apiKey,
@@ -397,7 +412,9 @@ function createClient(
397
412
  dangerouslyAllowBrowser: true,
398
413
  maxRetries: 5,
399
414
  defaultHeaders: headers,
400
- fetch: onSseEvent ? wrapFetchForSseDebug(baseFetch, event => onSseEvent(event, model)) : baseFetch,
415
+ fetch: onSseEvent
416
+ ? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
417
+ : transformedFetch,
401
418
  }),
402
419
  copilotPremiumRequests,
403
420
  baseUrl,
@@ -453,7 +470,7 @@ function buildParams(
453
470
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
454
471
  const promptCacheKey = getOpenAIResponsesCacheSessionId(options);
455
472
  const params: OpenAIResponsesSamplingParams = {
456
- model: model.id,
473
+ model: model.wireModelId ?? model.id,
457
474
  input: messages,
458
475
  instructions: systemInstructions,
459
476
  stream: true,
@@ -490,6 +507,7 @@ function buildParams(
490
507
  applyResponsesReasoningParams(params, model, options, messages, effort =>
491
508
  mapReasoningEffort(effort as NonNullable<OpenAIResponsesOptions["reasoning"]>, model.compat?.reasoningEffortMap),
492
509
  );
510
+ applyOpenAIRequestTransformBody(params, model.requestTransform);
493
511
 
494
512
  return { conversationMessages, params };
495
513
  }
package/src/types.ts CHANGED
@@ -823,6 +823,18 @@ export interface VercelGatewayRouting {
823
823
  }
824
824
 
825
825
  // Model interface for the unified model system
826
+
827
+ export interface ModelRequestTransform {
828
+ /** Named request-shaping preset. `openai-proxy` removes OpenAI SDK telemetry headers and uses a generic Gajae-Code User-Agent. */
829
+ profile?: "openai-proxy";
830
+ /** Header names to remove from the final outbound request. Case-insensitive. */
831
+ stripHeaders?: string[];
832
+ /** Headers to set after stripping; use null to remove a header explicitly. */
833
+ setHeaders?: Record<string, string | null>;
834
+ /** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
835
+ extraBody?: Record<string, unknown>;
836
+ }
837
+
826
838
  export interface Model<TApi extends Api = any> {
827
839
  id: string;
828
840
  name: string;
@@ -861,6 +873,10 @@ export interface Model<TApi extends Api = any> {
861
873
  preferWebsockets?: boolean;
862
874
  /** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
863
875
  contextPromotionTarget?: string;
876
+ /** Provider-facing model id when it differs from the local selector id. */
877
+ wireModelId?: string;
878
+ /** Declarative request shaping for OpenAI-compatible proxy providers. */
879
+ requestTransform?: ModelRequestTransform;
864
880
  /** Provider-assigned priority value (lower = higher priority). */
865
881
  priority?: number;
866
882
  /** Canonical thinking capability metadata for this model. */