feat(openai-auth): add OpenAI Max device-code authentication and unified prompt caching helpers

This commit is contained in:
2026-05-14 11:27:41 +00:00
parent cdf1e2bd99
commit 9ee5990321
14 changed files with 2064 additions and 513 deletions
+39
View File
@@ -1,6 +1,13 @@
export { getModel, getModelSetup } from './smartai.classes.smartai.js';
export type {
IOpenAiProviderOptions,
IOpenAiMaxAuthCredentials,
IOpenAiMaxAuthOptions,
IOpenAiMaxCompleteDeviceCodeOptions,
IOpenAiMaxDeviceCode,
IOpenAiMaxDeviceCodePollOptions,
IOpenAiMaxIdTokenInfo,
IOpenAiMaxTokenData,
ISmartAiModelSetup,
ISmartAiOptions,
TOpenAiReasoningEffort,
@@ -9,9 +16,41 @@ export type {
TSmartAiProviderOptions,
IOllamaModelOptions,
LanguageModelV3,
LanguageModelV3Prompt,
} from './smartai.interfaces.js';
export { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js';
export {
applySmartAiCacheProviderOptions,
applySmartAiPromptCaching,
createSmartAiCachingMiddleware,
getSmartAiCacheProviderOptions,
getSmartAiMessageCacheProviderOptions,
mergeSmartAiProviderOptions,
resolveSmartAiCacheProvider,
} from './smartai.cache.js';
export type {
ISmartAiCacheOptions,
TSmartAiCacheRetention,
TSmartAiCacheSetting,
TSmartAiMessageCacheProvider,
} from './smartai.cache.js';
export { createOllamaModel } from './smartai.provider.ollama.js';
export {
OPENAI_MAX_AUTH_ISSUER,
OPENAI_MAX_CLIENT_ID,
OPENAI_MAX_CODEX_BASE_URL,
OPENAI_MAX_DEFAULT_ORIGINATOR,
OpenAiMaxAuthError,
completeOpenAiMaxDeviceCodeLogin,
createOpenAiMaxProviderSettings,
ensureOpenAiMaxWorkspaceAllowed,
exchangeOpenAiMaxAuthorizationCode,
parseOpenAiMaxIdToken,
pollOpenAiMaxDeviceCode,
refreshOpenAiMaxTokenData,
requestOpenAiMaxDeviceCode,
} from './smartai.auth.openai.js';
export type { IOpenAiMaxAuthorizationCode } from './smartai.auth.openai.js';
// Re-export commonly used ai-sdk functions for consumer convenience
export { generateText, streamText, tool, jsonSchema } from 'ai';
+303
View File
@@ -0,0 +1,303 @@
import type {
IOpenAiMaxAuthCredentials,
IOpenAiMaxAuthOptions,
IOpenAiMaxCompleteDeviceCodeOptions,
IOpenAiMaxDeviceCode,
IOpenAiMaxDeviceCodePollOptions,
IOpenAiMaxIdTokenInfo,
IOpenAiMaxTokenData,
} from './smartai.interfaces.js';
export const OPENAI_MAX_AUTH_ISSUER = 'https://auth.openai.com';
export const OPENAI_MAX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
export const OPENAI_MAX_CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
export const OPENAI_MAX_DEFAULT_ORIGINATOR = 'smartai';
const DEVICE_CODE_TIMEOUT_MS = 15 * 60 * 1000;
export class OpenAiMaxAuthError extends Error {
public status?: number;
public body?: string;
constructor(message: string, options: { status?: number; body?: string } = {}) {
super(message);
this.name = 'OpenAiMaxAuthError';
this.status = options.status;
this.body = options.body;
}
}
export interface IOpenAiMaxAuthorizationCode {
authorizationCode: string;
codeChallenge: string;
codeVerifier: string;
}
interface IOpenAiMaxTokenResponse {
id_token?: unknown;
access_token?: unknown;
refresh_token?: unknown;
}
function getFetch(options: IOpenAiMaxAuthOptions): typeof fetch {
const fetchFunction = options.fetch ?? globalThis.fetch;
if (!fetchFunction) {
throw new OpenAiMaxAuthError('fetch is not available for OpenAI Max authentication.');
}
return fetchFunction;
}
function getIssuer(options: IOpenAiMaxAuthOptions): string {
return (options.issuer ?? OPENAI_MAX_AUTH_ISSUER).replace(/\/+$/, '');
}
function getClientId(options: IOpenAiMaxAuthOptions): string {
return options.clientId ?? OPENAI_MAX_CLIENT_ID;
}
function asString(value: unknown, name: string): string {
if (typeof value !== 'string' || value.length === 0) {
throw new OpenAiMaxAuthError(`OpenAI Max auth response is missing ${name}.`);
}
return value;
}
function asOptionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function asIntervalSeconds(value: unknown): number {
const interval = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
if (!Number.isFinite(interval) || interval <= 0) {
throw new OpenAiMaxAuthError('OpenAI Max device-code response has an invalid interval.');
}
return interval;
}
async function readJson(response: Response, context: string): Promise<unknown> {
const body = await response.text();
if (!response.ok) {
throw new OpenAiMaxAuthError(`${context} failed with status ${response.status}.`, {
status: response.status,
body,
});
}
try {
return body ? JSON.parse(body) : {};
} catch (error) {
throw new OpenAiMaxAuthError(`${context} returned invalid JSON: ${(error as Error).message}`, {
status: response.status,
body,
});
}
}
async function postJson(url: string, body: unknown, options: IOpenAiMaxAuthOptions): Promise<unknown> {
const response = await getFetch(options)(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return readJson(response, `POST ${url}`);
}
async function postForm(url: string, body: URLSearchParams, options: IOpenAiMaxAuthOptions): Promise<unknown> {
const response = await getFetch(options)(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
return readJson(response, `POST ${url}`);
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function parseJwtPayload(jwt: string): Record<string, unknown> {
const parts = jwt.split('.');
if (parts.length !== 3 || !parts[1]) {
throw new OpenAiMaxAuthError('OpenAI Max auth returned an invalid ID token.');
}
try {
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as Record<string, unknown>;
} catch (error) {
throw new OpenAiMaxAuthError(`OpenAI Max ID token could not be parsed: ${(error as Error).message}`);
}
}
export function parseOpenAiMaxIdToken(idToken: string): IOpenAiMaxIdTokenInfo {
const claims = parseJwtPayload(idToken);
const profile = claims['https://api.openai.com/profile'] as Record<string, unknown> | undefined;
const auth = claims['https://api.openai.com/auth'] as Record<string, unknown> | undefined;
const expiresAtSeconds = typeof claims.exp === 'number' ? claims.exp : undefined;
return {
email: asOptionalString(claims.email) ?? asOptionalString(profile?.email),
chatgptPlanType: asOptionalString(auth?.chatgpt_plan_type),
chatgptUserId: asOptionalString(auth?.chatgpt_user_id) ?? asOptionalString(auth?.user_id),
chatgptAccountId: asOptionalString(auth?.chatgpt_account_id),
chatgptAccountIsFedramp: auth?.chatgpt_account_is_fedramp === true,
expiresAt: expiresAtSeconds ? new Date(expiresAtSeconds * 1000).toISOString() : undefined,
rawJwt: idToken,
};
}
function createTokenData(response: IOpenAiMaxTokenResponse): IOpenAiMaxTokenData {
const idToken = asString(response.id_token, 'id_token');
const accessToken = asString(response.access_token, 'access_token');
const refreshToken = asString(response.refresh_token, 'refresh_token');
const idTokenInfo = parseOpenAiMaxIdToken(idToken);
return {
idToken,
accessToken,
refreshToken,
accountId: idTokenInfo.chatgptAccountId,
idTokenInfo,
};
}
export async function requestOpenAiMaxDeviceCode(
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxDeviceCode> {
const issuer = getIssuer(options);
const response = await postJson(`${issuer}/api/accounts/deviceauth/usercode`, {
client_id: getClientId(options),
}, options) as Record<string, unknown>;
return {
verificationUrl: `${issuer}/codex/device`,
userCode: asString(response.user_code ?? response.usercode, 'user_code'),
deviceAuthId: asString(response.device_auth_id, 'device_auth_id'),
intervalSeconds: asIntervalSeconds(response.interval),
};
}
export async function pollOpenAiMaxDeviceCode(
deviceCode: IOpenAiMaxDeviceCode,
options: IOpenAiMaxDeviceCodePollOptions = {},
): Promise<IOpenAiMaxAuthorizationCode> {
const issuer = getIssuer(options);
const pollUrl = `${issuer}/api/accounts/deviceauth/token`;
const timeoutMs = options.timeoutMs ?? DEVICE_CODE_TIMEOUT_MS;
const sleepFunction = options.sleep ?? sleep;
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const response = await getFetch(options)(pollUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device_auth_id: deviceCode.deviceAuthId,
user_code: deviceCode.userCode,
}),
});
if (response.ok) {
const body = await readJson(response, `POST ${pollUrl}`) as Record<string, unknown>;
return {
authorizationCode: asString(body.authorization_code, 'authorization_code'),
codeChallenge: asString(body.code_challenge, 'code_challenge'),
codeVerifier: asString(body.code_verifier, 'code_verifier'),
};
}
if (response.status !== 403 && response.status !== 404) {
const body = await response.text();
throw new OpenAiMaxAuthError(`OpenAI Max device-code polling failed with status ${response.status}.`, {
status: response.status,
body,
});
}
await response.arrayBuffer().catch(() => undefined);
const remaining = timeoutMs - (Date.now() - startedAt);
await sleepFunction(Math.min(deviceCode.intervalSeconds * 1000, Math.max(remaining, 0)));
}
throw new OpenAiMaxAuthError('OpenAI Max device-code login timed out.');
}
export async function exchangeOpenAiMaxAuthorizationCode(
authorizationCode: IOpenAiMaxAuthorizationCode,
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxTokenData> {
const issuer = getIssuer(options);
const response = await postForm(`${issuer}/oauth/token`, new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode.authorizationCode,
redirect_uri: `${issuer}/deviceauth/callback`,
client_id: getClientId(options),
code_verifier: authorizationCode.codeVerifier,
}), options) as IOpenAiMaxTokenResponse;
return createTokenData(response);
}
export function ensureOpenAiMaxWorkspaceAllowed(
tokenData: IOpenAiMaxTokenData,
forcedChatGptWorkspaceId?: string,
): void {
if (!forcedChatGptWorkspaceId) {
return;
}
if (tokenData.idTokenInfo.chatgptAccountId !== forcedChatGptWorkspaceId) {
throw new OpenAiMaxAuthError(`OpenAI Max login is restricted to workspace ${forcedChatGptWorkspaceId}.`);
}
}
export async function completeOpenAiMaxDeviceCodeLogin(
deviceCode: IOpenAiMaxDeviceCode,
options: IOpenAiMaxCompleteDeviceCodeOptions = {},
): Promise<IOpenAiMaxTokenData> {
const authorizationCode = await pollOpenAiMaxDeviceCode(deviceCode, options);
const tokenData = await exchangeOpenAiMaxAuthorizationCode(authorizationCode, options);
ensureOpenAiMaxWorkspaceAllowed(tokenData, options.forcedChatGptWorkspaceId);
return tokenData;
}
export async function refreshOpenAiMaxTokenData(
tokenData: IOpenAiMaxTokenData,
options: IOpenAiMaxAuthOptions = {},
): Promise<IOpenAiMaxTokenData> {
const issuer = getIssuer(options);
const response = await postJson(`${issuer}/oauth/token`, {
client_id: getClientId(options),
grant_type: 'refresh_token',
refresh_token: tokenData.refreshToken,
}, options) as IOpenAiMaxTokenResponse;
return createTokenData({
id_token: response.id_token ?? tokenData.idToken,
access_token: response.access_token ?? tokenData.accessToken,
refresh_token: response.refresh_token ?? tokenData.refreshToken,
});
}
export function createOpenAiMaxProviderSettings(credentials: IOpenAiMaxAuthCredentials): {
apiKey: string;
baseURL: string;
headers: Record<string, string>;
} {
const accountId = credentials.accountId ?? credentials.idTokenInfo?.chatgptAccountId;
const isFedrampAccount = credentials.idTokenInfo?.chatgptAccountIsFedramp === true;
const headers: Record<string, string> = {
originator: credentials.originator ?? OPENAI_MAX_DEFAULT_ORIGINATOR,
};
if (accountId) {
headers['ChatGPT-Account-ID'] = accountId;
}
if (isFedrampAccount) {
headers['X-OpenAI-Fedramp'] = 'true';
}
return {
apiKey: credentials.accessToken,
baseURL: credentials.baseUrl ?? OPENAI_MAX_CODEX_BASE_URL,
headers,
};
}
+250
View File
@@ -0,0 +1,250 @@
import type { JSONObject, JSONValue, LanguageModelV3Middleware, LanguageModelV3Prompt } from '@ai-sdk/provider';
import type { TSmartAiProviderOptions } from './smartai.interfaces.js';
export type TSmartAiMessageCacheProvider =
| 'anthropic'
| 'openrouter'
| 'bedrock'
| 'openaiCompatible'
| 'copilot'
| 'alibaba';
export type TSmartAiCacheRetention = 'ephemeral' | '1h' | 'in_memory' | '24h';
export interface ISmartAiCacheOptions {
/** Provider-specific message cache marker namespace. Usually inferred from the model. */
provider?: TSmartAiMessageCacheProvider;
/** Stable session/request key for providers that support request-level prompt cache affinity. */
key?: string;
/** Short retention is the default; longer retention is opt-in. */
retention?: TSmartAiCacheRetention;
}
export type TSmartAiCacheSetting = boolean | 'auto' | ISmartAiCacheOptions;
function isObject(input: unknown): input is Record<string, unknown> {
return typeof input === 'object' && input !== null && !Array.isArray(input);
}
function mergeJsonDefaults(defaults: JSONObject, overrides?: JSONObject): JSONObject {
const result: JSONObject = { ...defaults };
if (!overrides) return result;
for (const [key, value] of Object.entries(overrides)) {
const existing = result[key];
if (isObject(existing) && isObject(value)) {
result[key] = mergeJsonDefaults(existing as JSONObject, value as JSONObject);
continue;
}
result[key] = value as JSONValue;
}
return result;
}
export function mergeSmartAiProviderOptions(
defaults?: TSmartAiProviderOptions,
overrides?: TSmartAiProviderOptions,
): TSmartAiProviderOptions | undefined {
if (!defaults) return overrides;
if (!overrides) return defaults;
return mergeJsonDefaults(defaults as JSONObject, overrides as JSONObject) as TSmartAiProviderOptions;
}
function cacheOptionsFromSetting(cache: TSmartAiCacheSetting | undefined): ISmartAiCacheOptions | undefined {
if (cache === false) return undefined;
if (cache === undefined || cache === true || cache === 'auto') return {};
return cache;
}
export function resolveSmartAiCacheProvider(provider?: string, modelId?: string): TSmartAiMessageCacheProvider | undefined {
const providerLower = provider?.toLowerCase() ?? '';
const modelLower = modelId?.toLowerCase() ?? '';
if (providerLower.includes('openrouter')) return 'openrouter';
if (providerLower.includes('bedrock')) return 'bedrock';
if (providerLower.includes('copilot')) return 'copilot';
if (providerLower.includes('alibaba')) return 'alibaba';
if (providerLower.includes('openai-compatible') || providerLower.includes('openaicompatible')) {
return 'openaiCompatible';
}
if (providerLower.includes('anthropic')) return 'anthropic';
if (modelLower.includes('claude') || modelLower.includes('anthropic')) return 'anthropic';
return undefined;
}
export function getSmartAiMessageCacheProviderOptions(
provider: TSmartAiMessageCacheProvider,
options: ISmartAiCacheOptions = {},
): TSmartAiProviderOptions {
const anthropicCacheControl: JSONObject = {
type: 'ephemeral',
...(options.retention === '1h' ? { ttl: '1h' } : {}),
};
const providerOptions: Record<TSmartAiMessageCacheProvider, JSONObject> = {
anthropic: {
anthropic: {
cacheControl: anthropicCacheControl,
},
},
openrouter: {
openrouter: {
cacheControl: { type: 'ephemeral' },
},
},
bedrock: {
bedrock: {
cachePoint: { type: 'default' },
},
},
openaiCompatible: {
openaiCompatible: {
cache_control: { type: 'ephemeral' },
},
},
copilot: {
copilot: {
copilot_cache_control: { type: 'ephemeral' },
},
},
alibaba: {
alibaba: {
cacheControl: { type: 'ephemeral' },
},
},
};
return providerOptions[provider] as TSmartAiProviderOptions;
}
function shouldUseMessageLevelOptions(provider: TSmartAiMessageCacheProvider): boolean {
return provider === 'anthropic' || provider === 'bedrock';
}
function applyProviderOptionsDefaults<T extends { providerOptions?: TSmartAiProviderOptions }>(
item: T,
defaults: TSmartAiProviderOptions,
): T {
return {
...item,
providerOptions: mergeSmartAiProviderOptions(defaults, item.providerOptions),
};
}
function isToolApprovalPart(part: unknown): boolean {
if (!isObject(part)) return false;
return part.type === 'tool-approval-request' || part.type === 'tool-approval-response';
}
function applyCacheToMessage(
message: LanguageModelV3Prompt[number],
provider: TSmartAiMessageCacheProvider,
options: ISmartAiCacheOptions,
): LanguageModelV3Prompt[number] {
const providerOptions = getSmartAiMessageCacheProviderOptions(provider, options);
const content = message.content;
if (!shouldUseMessageLevelOptions(provider) && Array.isArray(content) && content.length > 0) {
const lastIndex = content.length - 1;
const lastPart = content[lastIndex];
if (!isToolApprovalPart(lastPart)) {
const messageWithArrayContent = message as Extract<LanguageModelV3Prompt[number], { content: unknown[] }>;
return {
...messageWithArrayContent,
content: content.map((part, index) =>
index === lastIndex ? applyProviderOptionsDefaults(part, providerOptions) : part,
) as typeof messageWithArrayContent.content,
} as LanguageModelV3Prompt[number];
}
}
return applyProviderOptionsDefaults(message, providerOptions);
}
export function applySmartAiPromptCaching(
prompt: LanguageModelV3Prompt,
options: ISmartAiCacheOptions = {},
): LanguageModelV3Prompt {
const provider = options.provider ?? 'anthropic';
const targetIndexes = new Set<number>();
const nonSystemIndexes: number[] = [];
let systemCount = 0;
for (let i = 0; i < prompt.length; i++) {
const message = prompt[i];
if (message.role === 'system') {
if (systemCount < 2) targetIndexes.add(i);
systemCount++;
continue;
}
nonSystemIndexes.push(i);
}
for (const index of nonSystemIndexes.slice(-2)) {
targetIndexes.add(index);
}
if (targetIndexes.size === 0) return prompt;
return prompt.map((message, index) =>
targetIndexes.has(index) ? applyCacheToMessage(message, provider, options) : message,
) as LanguageModelV3Prompt;
}
export function createSmartAiCachingMiddleware(options: ISmartAiCacheOptions = {}): LanguageModelV3Middleware {
return {
specificationVersion: 'v3',
transformParams: async ({ params }) => ({
...params,
prompt: applySmartAiPromptCaching(params.prompt, options),
}),
};
}
function isOpenAiProvider(provider?: string): boolean {
const providerLower = provider?.toLowerCase() ?? '';
return providerLower === 'openai' || providerLower.startsWith('openai.') || providerLower.includes('@ai-sdk/openai');
}
export function getSmartAiCacheProviderOptions(input: {
provider?: string;
modelId?: string;
cache?: TSmartAiCacheSetting;
sessionId?: string;
}): TSmartAiProviderOptions | undefined {
const cacheOptions = cacheOptionsFromSetting(input.cache);
if (!cacheOptions) return undefined;
if (isOpenAiProvider(input.provider)) {
const key = cacheOptions.key ?? input.sessionId;
return {
openai: {
store: false,
...(key ? { promptCacheKey: key } : {}),
...(cacheOptions.retention === '24h' || cacheOptions.retention === 'in_memory'
? { promptCacheRetention: cacheOptions.retention }
: key
? { promptCacheRetention: 'in_memory' }
: {}),
},
};
}
return undefined;
}
export function applySmartAiCacheProviderOptions(input: {
provider?: string;
modelId?: string;
providerOptions?: TSmartAiProviderOptions;
cache?: TSmartAiCacheSetting;
sessionId?: string;
}): TSmartAiProviderOptions | undefined {
return mergeSmartAiProviderOptions(
getSmartAiCacheProviderOptions(input),
input.providerOptions,
);
}
+9 -2
View File
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
import type { ISmartAiModelSetup, ISmartAiOptions, LanguageModelV3 } from './smartai.interfaces.js';
import { createOllamaModel } from './smartai.provider.ollama.js';
import { createAnthropicCachingMiddleware } from './smartai.middleware.anthropic.js';
import { createOpenAiMaxProviderSettings } from './smartai.auth.openai.js';
/**
* Returns a LanguageModelV3 for the given provider and model.
@@ -16,11 +17,17 @@ export function getModel(options: ISmartAiOptions): LanguageModelV3 {
if (options.promptCaching === false) return base;
return plugins.wrapLanguageModel({
model: base,
middleware: createAnthropicCachingMiddleware(),
middleware: createAnthropicCachingMiddleware(
typeof options.promptCaching === 'object' ? options.promptCaching : undefined,
),
}) as unknown as LanguageModelV3;
}
case 'openai': {
const p = plugins.createOpenAI({ apiKey: options.apiKey });
const p = plugins.createOpenAI(
options.openAiMaxAuth
? createOpenAiMaxProviderSettings(options.openAiMaxAuth)
: { apiKey: options.apiKey },
);
return p(options.model) as LanguageModelV3;
}
case 'google': {
+57 -3
View File
@@ -1,4 +1,5 @@
import type { JSONObject, JSONValue, LanguageModelV3 } from '@ai-sdk/provider';
import type { JSONObject, JSONValue, LanguageModelV3, LanguageModelV3Prompt } from '@ai-sdk/provider';
import type { ISmartAiCacheOptions } from './smartai.cache.js';
export type TProvider =
| 'anthropic'
@@ -14,6 +15,54 @@ export type TOpenAiReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'hi
export type TOpenAiTextVerbosity = 'low' | 'medium' | 'high';
export interface IOpenAiMaxIdTokenInfo {
email?: string;
chatgptPlanType?: string;
chatgptUserId?: string;
chatgptAccountId?: string;
chatgptAccountIsFedramp: boolean;
expiresAt?: string;
rawJwt: string;
}
export interface IOpenAiMaxAuthCredentials {
accessToken: string;
refreshToken?: string;
idToken?: string;
accountId?: string;
idTokenInfo?: IOpenAiMaxIdTokenInfo;
baseUrl?: string;
originator?: string;
}
export interface IOpenAiMaxTokenData extends IOpenAiMaxAuthCredentials {
refreshToken: string;
idToken: string;
idTokenInfo: IOpenAiMaxIdTokenInfo;
}
export interface IOpenAiMaxDeviceCode {
verificationUrl: string;
userCode: string;
deviceAuthId: string;
intervalSeconds: number;
}
export interface IOpenAiMaxAuthOptions {
issuer?: string;
clientId?: string;
fetch?: typeof fetch;
}
export interface IOpenAiMaxDeviceCodePollOptions extends IOpenAiMaxAuthOptions {
timeoutMs?: number;
sleep?: (ms: number) => Promise<void>;
}
export interface IOpenAiMaxCompleteDeviceCodeOptions extends IOpenAiMaxDeviceCodePollOptions {
forcedChatGptWorkspaceId?: string;
}
export interface IOpenAiProviderOptions extends JSONObject {
conversation?: string | null;
include?: string[] | null;
@@ -55,6 +104,11 @@ export interface ISmartAiOptions {
provider: TProvider;
model: string;
apiKey?: string;
/**
* OpenAI ChatGPT/Codex subscription credentials from the device-code auth flow.
* Only used when provider === 'openai'.
*/
openAiMaxAuth?: IOpenAiMaxAuthCredentials;
/**
* Provider-specific AI SDK generation options.
* Pass this to generateText()/streamText() alongside the model.
@@ -71,7 +125,7 @@ export interface ISmartAiOptions {
* Enable Anthropic prompt caching on system + recent messages.
* Only used when provider === 'anthropic'. Default: true.
*/
promptCaching?: boolean;
promptCaching?: boolean | ISmartAiCacheOptions;
}
/**
@@ -96,4 +150,4 @@ export interface IOllamaModelOptions {
think?: boolean;
}
export type { LanguageModelV3 };
export type { LanguageModelV3, LanguageModelV3Prompt };
+5 -31
View File
@@ -1,38 +1,12 @@
import type { LanguageModelV3Middleware, LanguageModelV3Prompt } from '@ai-sdk/provider';
import type { LanguageModelV3Middleware } from '@ai-sdk/provider';
import { createSmartAiCachingMiddleware } from './smartai.cache.js';
import type { ISmartAiCacheOptions } from './smartai.cache.js';
/**
* Creates middleware that adds Anthropic prompt caching directives.
* Marks the last system message and last user message with ephemeral cache control,
* reducing input token cost and latency on repeated calls.
*/
export function createAnthropicCachingMiddleware(): LanguageModelV3Middleware {
return {
specificationVersion: 'v3',
transformParams: async ({ params }) => {
const messages = [...params.prompt] as Array<Record<string, unknown>>;
// Find the last system message and last user message
let lastSystemIdx = -1;
let lastUserIdx = -1;
for (let i = 0; i < messages.length; i++) {
if (messages[i].role === 'system') lastSystemIdx = i;
if (messages[i].role === 'user') lastUserIdx = i;
}
const targets = [lastSystemIdx, lastUserIdx].filter(i => i >= 0);
for (const idx of targets) {
const msg = { ...messages[idx] };
msg.providerOptions = {
...(msg.providerOptions as Record<string, unknown> || {}),
anthropic: {
...((msg.providerOptions as Record<string, unknown>)?.anthropic as Record<string, unknown> || {}),
cacheControl: { type: 'ephemeral' },
},
};
messages[idx] = msg;
}
return { ...params, prompt: messages as unknown as LanguageModelV3Prompt };
},
};
export function createAnthropicCachingMiddleware(options: ISmartAiCacheOptions = {}): LanguageModelV3Middleware {
return createSmartAiCachingMiddleware({ ...options, provider: 'anthropic' });
}