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
+12 -7
View File
@@ -11,12 +11,17 @@
"projectDomain": "push.rocks"
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
"targets": {
"npm": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
}
},
"schemaVersion": 2
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
@@ -24,4 +29,4 @@
"@ship.zone/szci": {
"npmGlobalTools": []
}
}
}
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## Pending
### Features
- add OpenAI Max device-code authentication and unified prompt caching helpers (openai-auth)
- adds OpenAI Max device-code login, token refresh, and ChatGPT Codex provider routing for OpenAI models
- introduces reusable SmartAI cache helpers and middleware for prompt/message cache provider options across providers
- updates Anthropic prompt caching to use opencode-style breakpoint selection with optional longer retention
- exports the new auth and caching APIs and adds tests and documentation for both features
## 2026-05-06 - 2.2.0 - feat(openai)
add getModelSetup() and typed provider options for OpenAI reasoning settings
+14 -14
View File
@@ -41,30 +41,30 @@
"buildDocs": "(tsdoc)"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^3.6.3",
"@git.zone/tsbuild": "^4.4.1",
"@git.zone/tsrun": "^2.0.4",
"@git.zone/tstest": "^3.6.6",
"@push.rocks/qenv": "^6.1.4",
"@types/json-schema": "^7.0.15",
"@types/lodash.clonedeep": "^4.5.9",
"@types/node": "^25.6.0",
"@types/node": "^25.7.0",
"@types/pngjs": "^6.0.5",
"typescript": "^6.0.3",
"undici-types": "^8.2.0"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.75",
"@ai-sdk/google": "^3.0.67",
"@ai-sdk/groq": "^3.0.38",
"@ai-sdk/mistral": "^3.0.35",
"@ai-sdk/openai": "^3.0.62",
"@ai-sdk/perplexity": "^3.0.32",
"@ai-sdk/anthropic": "^3.0.77",
"@ai-sdk/google": "^3.0.73",
"@ai-sdk/groq": "^3.0.39",
"@ai-sdk/mistral": "^3.0.36",
"@ai-sdk/openai": "^3.0.63",
"@ai-sdk/perplexity": "^3.0.33",
"@ai-sdk/provider": "^3.0.10",
"@ai-sdk/xai": "^3.0.88",
"@anthropic-ai/sdk": "0.95.0",
"@ai-sdk/xai": "^3.0.89",
"@anthropic-ai/sdk": "0.95.2",
"@push.rocks/smartpdf": "^4.2.2",
"ai": "^6.0.175",
"openai": "^6.36.0"
"ai": "^6.0.180",
"openai": "^6.37.0"
},
"repository": {
"type": "git",
+1048 -453
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -10,6 +10,7 @@ The package is a **provider registry** built on the Vercel AI SDK (`ai` v6). The
- Providers: anthropic, openai, google, groq, mistral, xai, perplexity, ollama
- Anthropic prompt caching via `wrapLanguageModel` middleware (enabled by default)
- Custom Ollama provider implementing `LanguageModelV3` directly (for think, num_ctx support)
- OpenAI ChatGPT/Max device-code auth in `smartai.auth.openai.ts`; `openAiMaxAuth` routes OpenAI models to the ChatGPT Codex backend
### Subpath Exports
- `@push.rocks/smartai/vision``analyzeImage()` using `generateText` with image content
@@ -43,6 +44,7 @@ The package is a **provider registry** built on the Vercel AI SDK (`ai` v6). The
- `qenv.getEnvVarOnDemand()` returns a Promise — must be awaited in tests
- OpenAI reasoning options belong in AI SDK `providerOptions`, not model construction options
- SmartAI accepts OpenAI model IDs as plain strings, including `gpt-5.5`
- ChatGPT-authenticated OpenAI model calls use `https://chatgpt.com/backend-api/codex` plus bearer access token and `ChatGPT-Account-ID`, not `https://api.openai.com/v1`
## Testing
+55 -3
View File
@@ -17,7 +17,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- **🔌 One function, eight providers** — `getModel()` returns a standard `LanguageModelV3`. Switch providers by changing a string.
- **🧱 Built on Vercel AI SDK** — Uses `ai` v6 under the hood. Your model works with `generateText()`, `streamText()`, tool calling, structured output, and everything else in the AI SDK ecosystem.
- **🏠 Custom Ollama provider** — A full `LanguageModelV3` implementation for Ollama with support for `think` mode, `num_ctx`, auto-tuned temperature for Qwen models, and native tool calling.
- **💰 Anthropic prompt caching** — Automatic `cacheControl` middleware reduces cost and latency on repeated calls. Enabled by default, opt out with `promptCaching: false`.
- **💰 Prompt caching** — Anthropic cache-control middleware is enabled by default; provider cache helpers are available for agent/session integrations.
- **📦 Modular subpath exports** — Vision, audio, image, document, and research capabilities ship as separate imports. Only import what you need.
- **⚡ Zero lock-in** — Your code uses standard AI SDK types. Swap providers without touching application logic.
@@ -107,6 +107,30 @@ console.log(result.text);
OpenAI `reasoningEffort` supports `'none'`, `'minimal'`, `'low'`, `'medium'`, `'high'`, and `'xhigh'`. Model IDs are accepted as strings, so new IDs like `'gpt-5.5'` can be used before upstream model unions are updated.
### OpenAI Max / ChatGPT Auth
SmartAI can request ChatGPT subscription-backed Codex credentials with OpenAI's device-code flow. The returned credentials are passed to `getModel()` through `openAiMaxAuth`; SmartAI then routes OpenAI model calls through the ChatGPT Codex backend with the required account headers.
```typescript
import {
completeOpenAiMaxDeviceCodeLogin,
getModel,
requestOpenAiMaxDeviceCode,
} from '@push.rocks/smartai';
const deviceCode = await requestOpenAiMaxDeviceCode();
console.log(`Open ${deviceCode.verificationUrl} and enter ${deviceCode.userCode}`);
const openAiMaxAuth = await completeOpenAiMaxDeviceCodeLogin(deviceCode);
const model = getModel({
provider: 'openai',
model: 'gpt-5.5',
openAiMaxAuth,
});
```
Use `refreshOpenAiMaxTokenData(openAiMaxAuth)` before stored credentials expire, or after receiving an unauthorized response.
### Re-exported AI SDK Functions
SmartAI re-exports the most commonly used functions from `ai` for convenience:
@@ -250,9 +274,9 @@ console.log(result.text);
- **Streaming with reasoning** — `doStream()` emits proper `reasoning-start`, `reasoning-delta`, `reasoning-end` parts alongside text.
- **All Ollama options** — `num_ctx`, `top_k`, `top_p`, `repeat_penalty`, `num_predict`, `stop`, `seed`.
## 💰 Anthropic Prompt Caching
## 💰 Prompt Caching
When using the Anthropic provider, SmartAI automatically wraps the model with caching middleware that adds `cacheControl: { type: 'ephemeral' }` to the last system message and last user message. This can significantly reduce cost and latency for repeated calls with the same system prompt.
When using the Anthropic provider, SmartAI automatically wraps the model with caching middleware. The middleware follows the same breakpoint strategy used by opencode: cache the first two system messages and the two most recent non-system messages. This can significantly reduce cost and latency for repeated agent calls with stable system/tool context.
```typescript
// Caching enabled by default
@@ -271,6 +295,17 @@ const modelNoCaching = getModel({
});
```
Longer Anthropic cache TTL is opt-in:
```typescript
const modelWithOneHourCache = getModel({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
apiKey: process.env.ANTHROPIC_TOKEN,
promptCaching: { retention: '1h' },
});
```
You can also use the middleware directly:
```typescript
@@ -281,6 +316,23 @@ const middleware = createAnthropicCachingMiddleware();
const cachedModel = wrapLanguageModel({ model: baseModel, middleware });
```
For agent frameworks, SmartAI exports lower-level helpers:
```typescript
import {
applySmartAiCacheProviderOptions,
createSmartAiCachingMiddleware,
} from '@push.rocks/smartai';
const providerOptions = applySmartAiCacheProviderOptions({
provider: 'openai',
sessionId: 'stable-session-id',
cache: 'auto',
});
```
OpenAI request-level cache affinity is only added when a stable `sessionId` or explicit cache `key` is provided. Extended OpenAI retention (`'24h'`) is opt-in.
## 📦 Subpath Exports
SmartAI provides specialized capabilities as separate subpath imports. Each one is a focused utility that takes a model (or API key) and does one thing well.
+209
View File
@@ -0,0 +1,209 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartai from '../ts/index.js';
import type { IOpenAiMaxTokenData } from '../ts/index.js';
interface IMockFetchRequest {
url: string;
init?: RequestInit;
}
function createJwt(payload: Record<string, unknown>): string {
const encode = (value: Record<string, unknown>) => Buffer.from(JSON.stringify(value)).toString('base64url');
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.sig`;
}
function createTokenData(accountId = 'workspace-1'): IOpenAiMaxTokenData {
const idToken = createJwt({
email: 'user@example.com',
exp: 4_102_444_800,
'https://api.openai.com/auth': {
chatgpt_plan_type: 'pro',
chatgpt_user_id: 'user-1',
chatgpt_account_id: accountId,
chatgpt_account_is_fedramp: false,
},
});
const idTokenInfo = smartai.parseOpenAiMaxIdToken(idToken);
return {
idToken,
accessToken: 'access-token',
refreshToken: 'refresh-token',
accountId,
idTokenInfo,
};
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
}
function getHeader(init: RequestInit | undefined, name: string): string | null {
return new Headers(init?.headers).get(name);
}
tap.test('requestOpenAiMaxDeviceCode requests a user code', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({ url: String(input), init });
return jsonResponse({
device_auth_id: 'device-1',
usercode: 'ABCD-EFGH',
interval: '2',
});
};
try {
const deviceCode = await smartai.requestOpenAiMaxDeviceCode({
issuer: 'https://auth.example.test',
clientId: 'client-1',
});
expect(deviceCode).toEqual({
verificationUrl: 'https://auth.example.test/codex/device',
userCode: 'ABCD-EFGH',
deviceAuthId: 'device-1',
intervalSeconds: 2,
});
expect(requests[0].url).toEqual('https://auth.example.test/api/accounts/deviceauth/usercode');
expect(JSON.parse(String(requests[0].init?.body))).toEqual({ client_id: 'client-1' });
} finally {
globalThis.fetch = originalFetch;
}
});
tap.test('completeOpenAiMaxDeviceCodeLogin polls and exchanges OAuth tokens', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
const tokenData = createTokenData('workspace-1');
const responses = [
jsonResponse({}, 403),
jsonResponse({
authorization_code: 'auth-code',
code_challenge: 'challenge',
code_verifier: 'verifier',
}),
jsonResponse({
id_token: tokenData.idToken,
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
}),
];
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({ url: String(input), init });
const response = responses.shift();
if (!response) throw new Error('Unexpected fetch call');
return response;
};
try {
const result = await smartai.completeOpenAiMaxDeviceCodeLogin({
verificationUrl: 'https://auth.example.test/codex/device',
userCode: 'ABCD-EFGH',
deviceAuthId: 'device-1',
intervalSeconds: 1,
}, {
issuer: 'https://auth.example.test',
clientId: 'client-1',
forcedChatGptWorkspaceId: 'workspace-1',
sleep: async () => undefined,
});
expect(result.accessToken).toEqual('access-token');
expect(result.refreshToken).toEqual('refresh-token');
expect(result.idTokenInfo.chatgptAccountId).toEqual('workspace-1');
expect(requests.length).toEqual(3);
expect(JSON.parse(String(requests[0].init?.body))).toEqual({
device_auth_id: 'device-1',
user_code: 'ABCD-EFGH',
});
const tokenExchangeBody = new URLSearchParams(String(requests[2].init?.body));
expect(tokenExchangeBody.get('grant_type')).toEqual('authorization_code');
expect(tokenExchangeBody.get('code')).toEqual('auth-code');
expect(tokenExchangeBody.get('redirect_uri')).toEqual('https://auth.example.test/deviceauth/callback');
expect(tokenExchangeBody.get('client_id')).toEqual('client-1');
expect(tokenExchangeBody.get('code_verifier')).toEqual('verifier');
} finally {
globalThis.fetch = originalFetch;
}
});
tap.test('refreshOpenAiMaxTokenData refreshes and preserves omitted token fields', async () => {
const originalFetch = globalThis.fetch;
const requests: IMockFetchRequest[] = [];
const tokenData = createTokenData('workspace-1');
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({ url: String(input), init });
return jsonResponse({ access_token: 'new-access-token' });
};
try {
const result = await smartai.refreshOpenAiMaxTokenData(tokenData, {
issuer: 'https://auth.example.test',
clientId: 'client-1',
});
expect(result.accessToken).toEqual('new-access-token');
expect(result.refreshToken).toEqual('refresh-token');
expect(result.idToken).toEqual(tokenData.idToken);
expect(JSON.parse(String(requests[0].init?.body))).toEqual({
client_id: 'client-1',
grant_type: 'refresh_token',
refresh_token: 'refresh-token',
});
} finally {
globalThis.fetch = originalFetch;
}
});
tap.test('getModel uses ChatGPT Codex backend for OpenAI Max auth', async () => {
const originalFetch = globalThis.fetch;
let capturedRequest: IMockFetchRequest | undefined;
const tokenData = createTokenData('workspace-1');
const model = smartai.getModel({
provider: 'openai',
model: 'gpt-5.5',
openAiMaxAuth: tokenData,
});
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
capturedRequest = { url: String(input), init };
return jsonResponse({
id: 'resp-1',
created_at: 1,
model: 'gpt-5.5',
output: [{
type: 'message',
role: 'assistant',
id: 'msg-1',
content: [{ type: 'output_text', text: 'ok', annotations: [] }],
}],
usage: {
input_tokens: 1,
output_tokens: 1,
},
});
};
try {
await model.doGenerate({
prompt: [{ role: 'user', content: [{ type: 'text', text: 'hello' }] }],
inputFormat: 'prompt',
} as any);
expect(capturedRequest?.url).toEqual('https://chatgpt.com/backend-api/codex/responses');
expect(getHeader(capturedRequest?.init, 'authorization')).toEqual('Bearer access-token');
expect(getHeader(capturedRequest?.init, 'chatgpt-account-id')).toEqual('workspace-1');
expect(getHeader(capturedRequest?.init, 'originator')).toEqual('smartai');
} finally {
globalThis.fetch = originalFetch;
}
});
export default tap.start();
+51
View File
@@ -2,6 +2,7 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as qenv from '@push.rocks/qenv';
import { simulateReadableStream } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';
import type { LanguageModelV3Prompt } from '@ai-sdk/provider';
import * as smartai from '../ts/index.js';
const testQenv = new qenv.Qenv('./', './.nogit/');
@@ -53,6 +54,56 @@ tap.test('getModel with anthropic prompt caching returns wrapped model', async (
expect(modelNoCaching).toHaveProperty('doGenerate');
});
tap.test('applySmartAiPromptCaching should mark opencode-style breakpoints', async () => {
const prompt: LanguageModelV3Prompt = [
{ role: 'system', content: 'stable system one' },
{ role: 'system', content: 'stable system two' },
{ role: 'system', content: 'uncached system three' },
{ role: 'user', content: [{ type: 'text', text: 'old user' }] },
{ role: 'assistant', content: [{ type: 'text', text: 'recent assistant' }] },
{ role: 'user', content: [{ type: 'text', text: 'recent user' }] },
];
const cached = smartai.applySmartAiPromptCaching(prompt, { provider: 'anthropic' });
expect((cached[0].providerOptions as any)?.anthropic?.cacheControl?.type).toEqual('ephemeral');
expect((cached[1].providerOptions as any)?.anthropic?.cacheControl?.type).toEqual('ephemeral');
expect((cached[2].providerOptions as any)?.anthropic).toBeUndefined();
expect((cached[3].providerOptions as any)?.anthropic).toBeUndefined();
expect((cached[4].providerOptions as any)?.anthropic?.cacheControl?.type).toEqual('ephemeral');
expect((cached[5].providerOptions as any)?.anthropic?.cacheControl?.type).toEqual('ephemeral');
});
tap.test('applySmartAiPromptCaching should use content-level options for compatible providers', async () => {
const prompt: LanguageModelV3Prompt = [
{ role: 'user', content: [{ type: 'text', text: 'cache this' }] },
];
const cached = smartai.applySmartAiPromptCaching(prompt, { provider: 'openaiCompatible' });
const userContent = cached[0].role === 'user' ? cached[0].content : [];
const lastPart = userContent[0];
expect((lastPart.providerOptions as any)?.openaiCompatible?.cache_control?.type).toEqual('ephemeral');
});
tap.test('applySmartAiCacheProviderOptions should add OpenAI cache defaults without overriding caller options', async () => {
const providerOptions = smartai.applySmartAiCacheProviderOptions({
provider: 'openai',
sessionId: 'session-123',
cache: 'auto',
providerOptions: {
openai: {
reasoningEffort: 'high',
},
},
});
expect(providerOptions?.openai?.store).toEqual(false);
expect(providerOptions?.openai?.promptCacheKey).toEqual('session-123');
expect(providerOptions?.openai?.promptCacheRetention).toEqual('in_memory');
expect(providerOptions?.openai?.reasoningEffort).toEqual('high');
});
tap.test('generateText with anthropic model', async () => {
const apiKey = await testQenv.getEnvVarOnDemand('ANTHROPIC_TOKEN');
if (!apiKey) {
+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' });
}