Skip to content

Commit 3bed78b

Browse files
committed
refactor(openai-base, ai-utils, ai-openai, ai-grok): address PR #409 review feedback
- Eliminate `toCompatibleConfig` casts: `OpenAIClientConfig`/`GrokClientConfig` extend `OpenAICompatibleClientConfig` directly; ai-grok keeps a `withGrokDefaults()` helper for its xAI baseURL default. - Fix tool-call streaming bugs: - chat-completions-text: track `emittedAnyToolCallEnd` at outer scope so a `tool_calls` finish_reason without a started/ended pair downgrades to `stop`. - responses-text: guard `function_call_arguments.delta` with `metadata?.started` to match the `.done` handler. - Schema converter: `originalRequired` is optional and falls back to `schema.required`; adapters now pass the unmodified array (not `|| []`) so root-level required fields are no longer silently widened to `null`. - Forward request options: shared `extractRequestOptions` helper threads `headers`/`signal` from `Request | RequestInit` into both Chat Completions and Responses SDK calls without per-call casts. - Restore logger calls dropped during the refactor: `logger.request` before every SDK call (image/summarize/transcription/tts/video/responses-text/ chat-completions-text), `logger.provider` per-chunk in both streaming adapters, and try/catch + `logger.errors` around `summarize`, `summarizeStream`, and `createVideoJob`. - Move base64 helpers into `@tanstack/ai-utils` (`arrayBufferToBase64`, `base64ToArrayBuffer`) with `Uint8Array.toBase64`/`fromBase64` fast paths and `Buffer`/`atob`/`btoa` fallbacks; remove the per-adapter copies. - Add `warnIfLargeMediaBuffer` in video.ts to surface base64-encoding-an- unbounded-blob OOM risks on Workers/Lambda. - Replace `client as any` in video.ts with a typed `getVideosClient()` accessor; drop the arbitrary 1-10 cap in base `validateNumberOfImages`. - Delete dead code: `openai-base/src/types/message-metadata.ts`, `provider-options.ts`, and the unused `defineModelMeta` in ai-utils. - Brand-via-base: every ai-openai tool factory now delegates to its `@tanstack/openai-base` counterpart and brands the result, removing ten duplicate function bodies. - `satisfies` instead of `as` for the structured-output response narrow. - Add `@types/node` dev dep on ai-utils so `process` typechecks. - Tests: new `media-adapters.test.ts` (image / summarize / transcription / tts / video) in openai-base, new `base64.test.ts` (incl. 1.5 MiB roundtrip) in ai-utils. All test suites green: ai-utils 18, openai-base 71, ai-groq 17, ai-openai 131, ai-grok 53. `test:types`, `test:eslint`, and `build` clean for every affected package.
1 parent d0b318d commit 3bed78b

41 files changed

Lines changed: 831 additions & 565 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/typescript/ai-grok/src/adapters/image.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleImageAdapter } from '@tanstack/openai-base'
2-
import { getGrokApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'
33
import {
44
validateImageSize,
55
validateNumberOfImages,
@@ -41,7 +41,7 @@ export class GrokImageAdapter<
4141
readonly name = 'grok' as const
4242

4343
constructor(config: GrokImageConfig, model: TModel) {
44-
super(toCompatibleConfig(config), model, 'grok')
44+
super(withGrokDefaults(config), model, 'grok')
4545
}
4646

4747
protected override validatePrompt(options: {

packages/typescript/ai-grok/src/adapters/text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleChatCompletionsTextAdapter } from '@tanstack/openai-base'
2-
import { getGrokApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getGrokApiKeyFromEnv, withGrokDefaults } from '../utils/client'
33
import type {
44
GROK_CHAT_MODELS,
55
GrokChatModelToolCapabilitiesByName,
@@ -56,7 +56,7 @@ export class GrokTextAdapter<
5656
readonly name = 'grok' as const
5757

5858
constructor(config: GrokTextConfig, model: TModel) {
59-
super(toCompatibleConfig(config), model, 'grok')
59+
super(withGrokDefaults(config), model, 'grok')
6060
}
6161
}
6262

packages/typescript/ai-grok/src/utils/client.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { getApiKeyFromEnv } from '@tanstack/ai-utils'
22
import type { OpenAICompatibleClientConfig } from '@tanstack/openai-base'
3-
import type { ClientOptions } from 'openai'
43

5-
export interface GrokClientConfig extends ClientOptions {
6-
apiKey: string
7-
}
4+
export interface GrokClientConfig extends OpenAICompatibleClientConfig {}
85

96
/**
107
* Gets Grok API key from environment variables
@@ -21,14 +18,14 @@ export function getGrokApiKeyFromEnv(): string {
2118
}
2219

2320
/**
24-
* Converts a GrokClientConfig to OpenAICompatibleClientConfig.
25-
* Sets the default xAI base URL if not already set.
21+
* Returns a Grok client config with the default xAI base URL applied
22+
* when not already set.
2623
*/
27-
export function toCompatibleConfig(
24+
export function withGrokDefaults(
2825
config: GrokClientConfig,
2926
): OpenAICompatibleClientConfig {
3027
return {
3128
...config,
3229
baseURL: config.baseURL || 'https://api.x.ai/v1',
33-
} as unknown as OpenAICompatibleClientConfig
30+
}
3431
}

packages/typescript/ai-grok/src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export { generateId } from '@tanstack/ai-utils'
22
export {
33
getGrokApiKeyFromEnv,
4-
toCompatibleConfig,
4+
withGrokDefaults,
55
type GrokClientConfig,
66
} from './client'
77
export {

packages/typescript/ai-openai/src/adapters/image.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleImageAdapter } from '@tanstack/openai-base'
2-
import { getOpenAIApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getOpenAIApiKeyFromEnv } from '../utils/client'
33
import {
44
validateImageSize,
55
validateNumberOfImages,
@@ -41,7 +41,7 @@ export class OpenAIImageAdapter<
4141
readonly name = 'openai' as const
4242

4343
constructor(config: OpenAIImageConfig, model: TModel) {
44-
super(toCompatibleConfig(config), model, 'openai')
44+
super(config, model, 'openai')
4545
}
4646

4747
protected override validatePrompt(options: {

packages/typescript/ai-openai/src/adapters/summarize.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { OpenAICompatibleSummarizeAdapter } from '@tanstack/openai-base'
22
import { getOpenAIApiKeyFromEnv } from '../utils/client'
33
import { OpenAITextAdapter } from './text'
4-
import type { ChatStreamCapable } from '@tanstack/openai-base'
54
import type { OpenAIChatModel } from '../model-meta'
65
import type { OpenAIClientConfig } from '../utils/client'
76

@@ -36,16 +35,7 @@ export class OpenAISummarizeAdapter<
3635
readonly name = 'openai' as const
3736

3837
constructor(config: OpenAISummarizeConfig, model: TModel) {
39-
// The text adapter accepts richer provider options than the summarize adapter needs,
40-
// but we only pass basic options (model, messages, systemPrompts, etc.) at call time.
41-
super(
42-
new OpenAITextAdapter(
43-
config,
44-
model,
45-
) as unknown as ChatStreamCapable<OpenAISummarizeProviderOptions>,
46-
model,
47-
'openai',
48-
)
38+
super(new OpenAITextAdapter(config, model), model, 'openai')
4939
}
5040
}
5141

packages/typescript/ai-openai/src/adapters/text.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OpenAICompatibleResponsesTextAdapter } from '@tanstack/openai-base'
22
import { validateTextProviderOptions } from '../text/text-provider-options'
33
import { convertToolsToProviderFormat } from '../tools'
4-
import { getOpenAIApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
4+
import { getOpenAIApiKeyFromEnv } from '../utils/client'
55
import type {
66
OPENAI_CHAT_MODELS,
77
OpenAIChatModel,
@@ -89,7 +89,7 @@ export class OpenAITextAdapter<
8989
readonly name = 'openai' as const
9090

9191
constructor(config: OpenAITextConfig, model: TModel) {
92-
super(toCompatibleConfig(config), model, 'openai')
92+
super(config, model, 'openai')
9393
}
9494

9595
/**
@@ -99,18 +99,13 @@ export class OpenAITextAdapter<
9999
* and to apply OpenAI-specific provider option validation.
100100
*/
101101
protected override mapOptionsToRequest(
102-
options: TextOptions,
102+
options: TextOptions<TProviderOptions>,
103103
): Omit<OpenAI_SDK.Responses.ResponseCreateParams, 'stream'> {
104+
// The structural type the validator expects is broader than what
105+
// `TProviderOptions` is bound to per-model, so narrow via the internal
106+
// shape rather than re-exposing it on the public override signature.
104107
const modelOptions = options.modelOptions as
105-
| Omit<
106-
InternalTextProviderOptions,
107-
| 'max_output_tokens'
108-
| 'tools'
109-
| 'metadata'
110-
| 'temperature'
111-
| 'input'
112-
| 'top_p'
113-
>
108+
| InternalTextProviderOptions
114109
| undefined
115110
const input = this.convertMessagesToInput(options.messages)
116111
if (modelOptions) {

packages/typescript/ai-openai/src/adapters/transcription.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleTranscriptionAdapter } from '@tanstack/openai-base'
2-
import { getOpenAIApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getOpenAIApiKeyFromEnv } from '../utils/client'
33
import type { OpenAITranscriptionModel } from '../model-meta'
44
import type { OpenAITranscriptionProviderOptions } from '../audio/transcription-provider-options'
55
import type { OpenAIClientConfig } from '../utils/client'
@@ -31,7 +31,7 @@ export class OpenAITranscriptionAdapter<
3131
readonly name = 'openai' as const
3232

3333
constructor(config: OpenAITranscriptionConfig, model: TModel) {
34-
super(toCompatibleConfig(config), model, 'openai')
34+
super(config, model, 'openai')
3535
}
3636

3737
protected override shouldDefaultToVerbose(model: string): boolean {

packages/typescript/ai-openai/src/adapters/tts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleTTSAdapter } from '@tanstack/openai-base'
2-
import { getOpenAIApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getOpenAIApiKeyFromEnv } from '../utils/client'
33
import {
44
validateAudioInput,
55
validateInstructions,
@@ -31,7 +31,7 @@ export class OpenAITTSAdapter<
3131
readonly name = 'openai' as const
3232

3333
constructor(config: OpenAITTSConfig, model: TModel) {
34-
super(toCompatibleConfig(config), model, 'openai')
34+
super(config, model, 'openai')
3535
}
3636

3737
protected override validateAudioInput(text: string): void {

packages/typescript/ai-openai/src/adapters/video.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { OpenAICompatibleVideoAdapter } from '@tanstack/openai-base'
2-
import { getOpenAIApiKeyFromEnv, toCompatibleConfig } from '../utils/client'
2+
import { getOpenAIApiKeyFromEnv } from '../utils/client'
33
import {
44
toApiSeconds,
55
validateVideoSeconds,
@@ -48,7 +48,7 @@ export class OpenAIVideoAdapter<
4848
readonly name = 'openai' as const
4949

5050
constructor(config: OpenAIVideoConfig, model: TModel) {
51-
super(toCompatibleConfig(config), model, 'openai')
51+
super(config, model, 'openai')
5252
}
5353

5454
protected override validateVideoSize(model: string, size?: string): void {

0 commit comments

Comments
 (0)