From a3c57fb14c97e8d98b4dfb23b6e91307f6694dab Mon Sep 17 00:00:00 2001 From: Stackbilt Date: Fri, 17 Apr 2026 06:24:21 -0500 Subject: [PATCH] fix: retire claude-3-haiku-20240307 and gpt-4o, add drift test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #44. Haiku 3 retires 2026-04-19 (2 days out); GPT-4o retired 2026-04-03. Both had active references in the public API surface, provider model lists, pricing tables, and test fixtures. Changes: - Mark `MODELS.GPT_4O` and `MODELS.CLAUDE_3_HAIKU` as `@deprecated` (values retained per additive-only OSS policy — removing the exports would be a breaking change; deprecation flags callers at compile time) - Remove retired IDs from `AnthropicProvider.models` + capabilities map - Remove `gpt-4o` and dead `gpt-4-turbo-preview` alias from `OpenAIProvider.models` + capabilities map - Migrate 41 Haiku-3 fixture IDs to `claude-haiku-4-5-20251001` and gpt-4o test fixtures to `gpt-4o-mini` - Add `model-drift.test.ts` guarding against future drift between each provider's advertised `models[]` and its capabilities map — the `gpt-4-turbo-preview` alias was caught this way Not changed: `gpt-4o-mini` (status not yet confirmed; still the factory default fallback at `factory.ts:1107`). Needs separate audit. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 14 +++++++ package.json | 2 +- src/__tests__/factory.test.ts | 14 +++---- src/__tests__/from-env.test.ts | 8 ++-- src/__tests__/model-drift.test.ts | 43 ++++++++++++++++++++ src/__tests__/response-format.test.ts | 12 +++--- src/__tests__/schema-drift.test.ts | 46 +++++++++++----------- src/__tests__/tool-call-validation.test.ts | 28 ++++++------- src/index.ts | 2 + src/providers/anthropic.ts | 12 +----- src/providers/openai.ts | 12 ------ 11 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 src/__tests__/model-drift.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fd4f936..1716cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to `@stackbilt/llm-providers` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/). Versions use [Semantic Versioning](https://semver.org/). +## [1.4.0] — 2026-04-17 + +### Deprecated +- **`MODELS.CLAUDE_3_HAIKU`** (`claude-3-haiku-20240307`) — Anthropic retires 2026-04-19. Migrate to `MODELS.CLAUDE_HAIKU_4_5` or `MODELS.CLAUDE_3_5_HAIKU`. Export retained; callers get a compile-time `@deprecated` warning. +- **`MODELS.GPT_4O`** (`gpt-4o`) — retired by OpenAI on 2026-04-03. Migrate to `MODELS.GPT_4O_MINI` or a current GPT-4 successor. Export retained; callers get a compile-time `@deprecated` warning. + +### Removed +- `claude-3-haiku-20240307` — dropped from `AnthropicProvider.models[]` and its capabilities/pricing table. Calls to this ID will fail at Anthropic's cutoff; keeping it advertised would mislead consumers. +- `gpt-4o` — dropped from `OpenAIProvider.models[]` and its capabilities/pricing table. +- `gpt-4-turbo-preview` — dead alias dropped from `OpenAIProvider.models[]` (no corresponding capabilities entry; caught by the new drift test). + +### Added +- **Model drift test** (`src/__tests__/model-drift.test.ts`) — asserts every provider's `models[]` array is symmetrically covered by its capabilities map. Prevents future retirement drift where a model is removed from one list but not the other. Runs across all 5 providers. + ## [1.3.0] — 2026-04-16 ### Added diff --git a/package.json b/package.json index cf29004..8a364db 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stackbilt/llm-providers", - "version": "1.3.0", + "version": "1.4.0", "description": "Multi-LLM failover with circuit breakers, cost tracking, and intelligent retry. Cloudflare Workers native.", "author": "Stackbilt ", "license": "Apache-2.0", diff --git a/src/__tests__/factory.test.ts b/src/__tests__/factory.test.ts index ceed2aa..3c8dfa8 100755 --- a/src/__tests__/factory.test.ts +++ b/src/__tests__/factory.test.ts @@ -49,7 +49,7 @@ const mockOpenAIProvider = { const mockAnthropicProvider = { name: 'anthropic', - models: ['claude-3-haiku-20240307', 'claude-3-sonnet-20240229'], + models: ['claude-haiku-4-5-20251001', 'claude-3-sonnet-20240229'], supportsStreaming: true, supportsTools: true, supportsBatching: false, @@ -57,14 +57,14 @@ const mockAnthropicProvider = { generateResponse: vi.fn().mockResolvedValue({ message: 'Anthropic response', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, cost: 0.002 }, - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', provider: 'anthropic', responseTime: 1200 } as LLMResponse), streamResponse: vi.fn(), getProviderBalance: vi.fn(), validateConfig: vi.fn().mockReturnValue(true), - getModels: vi.fn().mockReturnValue(['claude-3-haiku-20240307', 'claude-3-sonnet-20240229']), + getModels: vi.fn().mockReturnValue(['claude-haiku-4-5-20251001', 'claude-3-sonnet-20240229']), estimateCost: vi.fn().mockReturnValue(0.002), healthCheck: vi.fn().mockResolvedValue(true), getMetrics: vi.fn().mockReturnValue({ @@ -161,7 +161,7 @@ describe('LLMProviderFactory', () => { mockAnthropicProvider.generateResponse.mockReset().mockResolvedValue({ message: 'Anthropic response', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30, cost: 0.002 }, - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', provider: 'anthropic', responseTime: 1200 } as LLMResponse); @@ -326,7 +326,7 @@ describe('LLMProviderFactory', () => { // Test Claude model request const claudeRequest: LLMRequest = { ...testRequest, - model: 'claude-3-haiku-20240307' + model: 'claude-haiku-4-5-20251001' }; await factory.generateResponse(claudeRequest); @@ -373,7 +373,7 @@ describe('LLMProviderFactory', () => { fallbackRules: [{ condition: 'error', fallbackProvider: 'anthropic', - fallbackModel: 'claude-3-haiku-20240307' + fallbackModel: 'claude-haiku-4-5-20251001' }] }); @@ -385,7 +385,7 @@ describe('LLMProviderFactory', () => { }); expect(mockAnthropicProvider.generateResponse).toHaveBeenCalledWith( - expect.objectContaining({ model: 'claude-3-haiku-20240307' }) + expect.objectContaining({ model: 'claude-haiku-4-5-20251001' }) ); }); }); diff --git a/src/__tests__/from-env.test.ts b/src/__tests__/from-env.test.ts index bcd581a..80bd453 100644 --- a/src/__tests__/from-env.test.ts +++ b/src/__tests__/from-env.test.ts @@ -13,13 +13,13 @@ const mockValidateConfig = vi.fn().mockReturnValue(true); vi.mock('../providers/openai', () => ({ OpenAIProvider: vi.fn().mockImplementation(() => ({ name: 'openai', - models: ['gpt-4o'], + models: ['gpt-4o-mini'], supportsStreaming: true, supportsTools: true, supportsBatching: true, validateConfig: mockValidateConfig, generateResponse: vi.fn(), - getModels: vi.fn().mockReturnValue(['gpt-4o']), + getModels: vi.fn().mockReturnValue(['gpt-4o-mini']), estimateCost: vi.fn().mockReturnValue(0), healthCheck: vi.fn().mockResolvedValue(true), getMetrics: vi.fn().mockReturnValue({ @@ -33,13 +33,13 @@ vi.mock('../providers/openai', () => ({ vi.mock('../providers/anthropic', () => ({ AnthropicProvider: vi.fn().mockImplementation(() => ({ name: 'anthropic', - models: ['claude-3-haiku-20240307'], + models: ['claude-haiku-4-5-20251001'], supportsStreaming: true, supportsTools: true, supportsBatching: false, validateConfig: mockValidateConfig, generateResponse: vi.fn(), - getModels: vi.fn().mockReturnValue(['claude-3-haiku-20240307']), + getModels: vi.fn().mockReturnValue(['claude-haiku-4-5-20251001']), estimateCost: vi.fn().mockReturnValue(0), healthCheck: vi.fn().mockResolvedValue(true), getMetrics: vi.fn().mockReturnValue({ diff --git a/src/__tests__/model-drift.test.ts b/src/__tests__/model-drift.test.ts new file mode 100644 index 0000000..bcfe8c6 --- /dev/null +++ b/src/__tests__/model-drift.test.ts @@ -0,0 +1,43 @@ +/** + * Model drift tests + * + * Guards against retired/unknown model IDs slipping through by verifying + * that every entry in a provider's `models` array has a matching entry in + * its capabilities map. A mismatch usually means a model was retired from + * one list but not the other — catch that at CI time, not at runtime. + */ + +import { describe, it, expect } from 'vitest'; +import { AnthropicProvider } from '../providers/anthropic'; +import { OpenAIProvider } from '../providers/openai'; +import { CloudflareProvider } from '../providers/cloudflare'; +import { CerebrasProvider } from '../providers/cerebras'; +import { GroqProvider } from '../providers/groq'; + +type WithCapabilities = { + models: string[]; + getModelCapabilities: () => Record; +}; + +const providers: Array<[string, WithCapabilities]> = [ + ['anthropic', new AnthropicProvider({ apiKey: 'test' }) as unknown as WithCapabilities], + ['openai', new OpenAIProvider({ apiKey: 'test' }) as unknown as WithCapabilities], + ['cloudflare', new CloudflareProvider({ ai: { run: async () => ({}) } as never, accountId: 'test' }) as unknown as WithCapabilities], + ['cerebras', new CerebrasProvider({ apiKey: 'test' }) as unknown as WithCapabilities], + ['groq', new GroqProvider({ apiKey: 'test' }) as unknown as WithCapabilities] +]; + +describe('model drift', () => { + it.each(providers)('%s: every advertised model has a capabilities entry', (_name, provider) => { + const caps = provider.getModelCapabilities(); + const missing = provider.models.filter((m) => !(m in caps)); + expect(missing).toEqual([]); + }); + + it.each(providers)('%s: every capabilities entry is advertised in models[]', (_name, provider) => { + const caps = provider.getModelCapabilities(); + const advertised = new Set(provider.models); + const orphaned = Object.keys(caps).filter((m) => !advertised.has(m)); + expect(orphaned).toEqual([]); + }); +}); diff --git a/src/__tests__/response-format.test.ts b/src/__tests__/response-format.test.ts index 31ad174..52efd6c 100644 --- a/src/__tests__/response-format.test.ts +++ b/src/__tests__/response-format.test.ts @@ -47,11 +47,11 @@ describe('OpenAI response_format', () => { }); it('should pass response_format through to the API body', async () => { - mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o')); + mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o-mini')); await provider.generateResponse({ messages: [{ role: 'user', content: 'Give me JSON' }], - model: 'gpt-4o', + model: 'gpt-4o-mini', response_format: { type: 'json_object' } }); @@ -60,11 +60,11 @@ describe('OpenAI response_format', () => { }); it('should not include response_format when not set', async () => { - mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o')); + mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o-mini')); await provider.generateResponse({ messages: [{ role: 'user', content: 'Hello' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); const body = JSON.parse(mockFetch.mock.calls[0][1].body); @@ -72,11 +72,11 @@ describe('OpenAI response_format', () => { }); it('should pass response_format with type text', async () => { - mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o', 'plain text')); + mockFetch.mockResolvedValueOnce(openAIChatCompletion('gpt-4o-mini', 'plain text')); await provider.generateResponse({ messages: [{ role: 'user', content: 'Hello' }], - model: 'gpt-4o', + model: 'gpt-4o-mini', response_format: { type: 'text' } }); diff --git a/src/__tests__/schema-drift.test.ts b/src/__tests__/schema-drift.test.ts index ca2a9bf..f3f28e2 100644 --- a/src/__tests__/schema-drift.test.ts +++ b/src/__tests__/schema-drift.test.ts @@ -147,7 +147,7 @@ describe('AnthropicProvider response schema validation', () => { type: 'message', role: 'assistant', content: [{ type: 'text', text: 'hello' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 }, }; @@ -161,7 +161,7 @@ describe('AnthropicProvider response schema validation', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); expect(res.content).toBe('hello'); @@ -181,7 +181,7 @@ describe('AnthropicProvider response schema validation', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ name: 'LLMProviderError', code: 'SCHEMA_DRIFT', @@ -200,7 +200,7 @@ describe('AnthropicProvider response schema validation', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content' }); }); @@ -213,7 +213,7 @@ describe('AnthropicProvider response schema validation', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content', @@ -236,7 +236,7 @@ describe('AnthropicProvider response schema validation', () => { await expect(retryProvider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT' }); expect(mockFetch).toHaveBeenCalledTimes(1); @@ -256,7 +256,7 @@ describe('LLMProviderFactory schema drift fallback', () => { const validOpenAIResponse = { id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { role: 'assistant', content: 'from openai' }, @@ -276,7 +276,7 @@ describe('LLMProviderFactory schema drift fallback', () => { type: 'message', role: 'assistant', // content intentionally missing — drift - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 }, }), @@ -317,7 +317,7 @@ describe('LLMProviderFactory schema drift fallback', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { prompt_tokens: 10 }, // wrong field name — drift on usage.input_tokens }), @@ -371,7 +371,7 @@ describe('LLMProviderFactory schema drift fallback', () => { ok: true, json: async () => ({ id: 'msg_1', type: 'message', role: 'assistant', - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 }, // content intentionally missing — drift @@ -429,7 +429,7 @@ describe('SchemaDriftError and circuit breaker', () => { json: async () => ({ id: 'msg_1', type: 'message', role: 'assistant', content: [], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { output_tokens: 5 }, }), @@ -442,7 +442,7 @@ describe('SchemaDriftError and circuit breaker', () => { try { await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); } catch { // Expected - drift or circuit-open @@ -473,7 +473,7 @@ describe('SchemaDriftError message surface (security)', () => { json: async () => ({ id: SECRET_VALUE, // intentionally put a "secret" where we expect a string // content missing — triggers drift on content path instead of id - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 }, }), @@ -483,7 +483,7 @@ describe('SchemaDriftError message surface (security)', () => { try { await provider.generateResponse({ messages: [{ role: 'user', content: SECRET_VALUE }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); expect.fail('should have thrown'); } catch (err) { @@ -518,7 +518,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { type: 'message', role: 'assistant', content, - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'end_turn', usage: { input_tokens: 10, output_tokens: 5 }, }); @@ -534,7 +534,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); expect(res.toolCalls).toHaveLength(1); }); @@ -550,7 +550,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content[0].id', @@ -570,7 +570,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content[0].input', @@ -590,7 +590,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content[0].text', @@ -606,7 +606,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content[0]', @@ -626,7 +626,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { await expect(provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', })).rejects.toMatchObject({ code: 'SCHEMA_DRIFT', path: 'content[0].type', @@ -648,7 +648,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); // Text content extracted; unknown block silently ignored by filter expect(res.content).toBe('hello'); @@ -664,7 +664,7 @@ describe('Anthropic nested content-block validation (H-2 / #42)', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', }); expect(res.content).toBe('hi'); }); diff --git a/src/__tests__/tool-call-validation.test.ts b/src/__tests__/tool-call-validation.test.ts index 6cba4dd..aecbb93 100644 --- a/src/__tests__/tool-call-validation.test.ts +++ b/src/__tests__/tool-call-validation.test.ts @@ -37,7 +37,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -58,7 +58,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'weather' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toHaveLength(1); @@ -73,7 +73,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -94,7 +94,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toBeUndefined(); @@ -105,7 +105,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -126,7 +126,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toBeUndefined(); @@ -137,7 +137,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -158,7 +158,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toBeUndefined(); @@ -169,7 +169,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -190,7 +190,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toBeUndefined(); @@ -201,7 +201,7 @@ describe('Tool call validation at provider boundary', () => { ok: true, json: async () => ({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'gpt-4o-mini', choices: [{ index: 0, message: { @@ -222,7 +222,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'hi' }], - model: 'gpt-4o' + model: 'gpt-4o-mini' }); expect(res.toolCalls).toHaveLength(2); @@ -250,7 +250,7 @@ describe('Tool call validation at provider boundary', () => { { type: 'tool_use', id: 'toolu_1', name: 'search', input: { q: 'test' } }, { type: 'tool_use', id: '', name: 'bad', input: {} } // empty id ], - model: 'claude-3-haiku-20240307', + model: 'claude-haiku-4-5-20251001', stop_reason: 'tool_use', usage: { input_tokens: 10, output_tokens: 5 } }), @@ -259,7 +259,7 @@ describe('Tool call validation at provider boundary', () => { const res = await provider.generateResponse({ messages: [{ role: 'user', content: 'search' }], - model: 'claude-3-haiku-20240307' + model: 'claude-haiku-4-5-20251001' }); // Only the valid tool call should survive diff --git a/src/index.ts b/src/index.ts index a2268ed..8e7e077 100755 --- a/src/index.ts +++ b/src/index.ts @@ -408,6 +408,7 @@ export const SUPPORTED_PROVIDERS = ['openai', 'anthropic', 'cloudflare', 'cerebr */ export const MODELS = { // OpenAI models + /** @deprecated Retired by OpenAI on 2026-04-03. Use GPT_4O_MINI or a current GPT-4 successor. */ GPT_4O: 'gpt-4o', GPT_4O_MINI: 'gpt-4o-mini', GPT_4_TURBO: 'gpt-4-turbo', @@ -425,6 +426,7 @@ export const MODELS = { CLAUDE_3_5_HAIKU: 'claude-3-5-haiku-20241022', CLAUDE_3_OPUS: 'claude-3-opus-20240229', CLAUDE_3_SONNET: 'claude-3-sonnet-20240229', + /** @deprecated Retires 2026-04-19. Use CLAUDE_HAIKU_4_5 or CLAUDE_3_5_HAIKU. */ CLAUDE_3_HAIKU: 'claude-3-haiku-20240307', // Cloudflare models diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 2565b5c..4caec23 100755 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -129,8 +129,7 @@ export class AnthropicProvider extends BaseProvider { 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229', - 'claude-3-sonnet-20240229', - 'claude-3-haiku-20240307' + 'claude-3-sonnet-20240229' ]; supportsStreaming = true; supportsTools = true; @@ -351,15 +350,6 @@ export class AnthropicProvider extends BaseProvider { inputTokenCost: 0.003, // $3 per 1M tokens outputTokenCost: 0.015, // $15 per 1M tokens description: 'Claude 3 Sonnet - Balanced performance' - }, - 'claude-3-haiku-20240307': { - maxContextLength: 200000, - supportsStreaming: true, - supportsTools: true, - supportsBatching: false, - inputTokenCost: 0.00025, // $0.25 per 1M tokens - outputTokenCost: 0.00125, // $1.25 per 1M tokens - description: 'Claude 3 Haiku - Fast and economical' } }; } diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 215d5a7..ca20956 100755 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -81,10 +81,8 @@ interface OpenAIResponse { export class OpenAIProvider extends BaseProvider { name = 'openai'; models = [ - 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', - 'gpt-4-turbo-preview', 'gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k' @@ -176,16 +174,6 @@ export class OpenAIProvider extends BaseProvider { protected getModelCapabilities(): Record { return { - 'gpt-4o': { - maxContextLength: 128000, - supportsStreaming: true, - supportsTools: true, - supportsVision: true, - supportsBatching: false, - inputTokenCost: 0.005, // $5 per 1M tokens - outputTokenCost: 0.015, // $15 per 1M tokens - description: 'GPT-4 Omni - Latest multimodal model' - }, 'gpt-4o-mini': { maxContextLength: 128000, supportsStreaming: true,