feat: add GPT-5.5 and Claude Opus 4.7 models

This commit is contained in:
Peter Steinberger 2026-05-10 09:43:33 +01:00
parent 3765b08186
commit 65d386c19e
No known key found for this signature in database
61 changed files with 409 additions and 465 deletions

View File

@ -19,10 +19,10 @@ All notable changes to the Tachikoma project will be documented in this file.
- Azure provider unit tests using URLProtocol stubs to verify path, query, and auth header construction.
### Changed
- Added OpenAI's GPT-5.1 family (flagship/mini/nano) throughout the model enums, selectors, provider factories, capability registry, pricing tables, docs, and test suites. GPT aliases (`gpt`, `gpt-5`, `gpt-4o`) now normalize to `.openai(.gpt51)` so downstream apps inherit the new default seamlessly.
- Added OpenAI's GPT-5.1 family (flagship/mini/nano) throughout the model enums, selectors, provider factories, capability registry, pricing tables, docs, and test suites. GPT aliases (`gpt`, `gpt-5`) now normalize to supported GPT-5 models so downstream apps inherit the new default seamlessly.
- Expanded xAI Grok support to the full November 2025 catalog (`grok-4-fast-*`, `grok-code-fast-1`, `grok-2-*`, `grok-vision-beta`, etc.), updated the CLI shortcuts so `grok` now maps to `grok-4-fast-reasoning`, and refreshed selectors, provider parsers, capability tables, and docs snippets to match the official API lineup.
- Google/Gemini support now targets the Gemini 2.5 family exclusively (`gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`), with updated model selectors, parsers, docs, and pricing tables; older 1.5/2.0 IDs are no longer recognized.
- Removed deprecated OpenAI reasoning models (`o1`, `o1-mini`, `o3`, `o3-mini`) in favour of the GPT5 family plus `o4-mini`, updating enums, provider factories, capability tables, prompts, and documentation metadata accordingly.
- Removed deprecated OpenAI reasoning models (`o1`, `o1-mini`, `o3`, `o3-mini`, `o4-mini`) in favour of the GPT5 family, updating enums, provider factories, capability tables, prompts, and documentation metadata accordingly.
- Google/Gemini integration now uses the documented `x-goog-api-key` header with `alt=sse` streaming, adds fallbacks for `GOOGLE_API_KEY` / `GOOGLE_APPLICATION_CREDENTIALS`, and hardens the SSE decoder so live tests succeed consistently.
- Pruned Anthropic model support to the Claude 4.x line (Opus 4, Sonnet 4 / 4.5, Haiku 4.5) to match current API availability and reduce maintenance burden.
- `TachikomaConfiguration` now loads credentials first and lets environment variables override them so operators can supersede stored settings without editing credentials files.
@ -71,8 +71,8 @@ All notable changes to the Tachikoma project will be documented in this file.
#### Provider Support
- **OpenAI Provider**: Complete integration with dual API support
- Chat Completions API for standard models (GPT-4o, GPT-4.1)
- Responses API for reasoning models (o3, o4 series)
- Chat Completions API for standard custom models
- Responses API for GPT-5 models
- Automatic API selection based on model capabilities
- Parameter filtering for reasoning models
- Full streaming support for both APIs
@ -80,7 +80,7 @@ All notable changes to the Tachikoma project will be documented in this file.
- **Anthropic Provider**: Native Claude API integration
- Support for Claude 4 (Opus, Sonnet) with thinking modes
- Claude 3.5/3.7 series compatibility
- Claude 4.x series compatibility
- Content block handling for multimodal inputs
- System prompt separation
- Server-Sent Events streaming

View File

@ -25,7 +25,7 @@ ai-cli "What is the capital of France?"
ai-cli --model claude "Explain quantum computing"
# Stream the response
ai-cli --stream --model gpt-4o "Write a short story"
ai-cli --stream --model gpt-5.5 "Write a short story"
```
## Parameters
@ -35,7 +35,7 @@ ai-cli --stream --model gpt-4o "Write a short story"
| `-m, --model <MODEL>` | Specify the AI model to use |
| `--api <chat\|responses>` | For OpenAI models: select API type (default: responses for GPT-5) |
| `-s, --stream` | Stream the response in real-time |
| `--thinking` | Show reasoning process (O3, O4, GPT-5 - note: API currently doesn't expose actual reasoning) |
| `--thinking` | Show GPT-5 reasoning process (note: API currently doesn't expose actual reasoning) |
| `--verbose, -v` | Show detailed debug output |
| `--config` | Show current configuration and API key status |
| `--help, -h` | Show help message |
@ -60,15 +60,11 @@ Add to your shell profile (`~/.zshrc`, `~/.bashrc`) for persistence.
## Supported Models
### OpenAI
- **GPT-5 Series**: `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
- **O-Series**: `o3`, `o3-mini`, `o3-pro`, `o4-mini`
- **GPT-4**: `gpt-4.1`, `gpt-4.1-mini`, `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`
- **Legacy**: `gpt-3.5-turbo`
- **GPT-5 Series**: `gpt-5.5`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
### Anthropic
- **Claude 4**: `claude-opus-4-1-20250805`, `claude-sonnet-4-20250514`
- **Claude 3.7**: `claude-3-7-sonnet`
- **Claude 3.5**: `claude-3-5-opus`, `claude-3-5-sonnet`, `claude-3-5-haiku`
- **Claude 4.x**: `claude-opus-4-7`, `claude-sonnet-4.5`, `claude-haiku-4.5`
### Google
- **Gemini 2.5**: `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
@ -81,7 +77,7 @@ Add to your shell profile (`~/.zshrc`, `~/.bashrc`) for persistence.
### Model Shortcuts
- `claude` → claude-opus-4-1-20250805
- `gpt` → gpt-4.1
- `gpt` → gpt-5.5
- `gemini` → gemini-2.5-flash
- `grok` → grok-4-fast-reasoning
- `llama` → llama3.3
@ -98,7 +94,7 @@ ai-cli --stream --model gpt-5 "Explain the theory of relativity"
# API selection for OpenAI
ai-cli --model gpt-5 --api chat "Use Chat Completions API"
ai-cli --model o3 --api responses "Use Responses API"
ai-cli --model gpt-5-thinking --api responses "Use Responses API"
# Debug mode
ai-cli --verbose --model opus "Debug this request"

View File

@ -190,7 +190,7 @@ struct AICLI {
-m, --model <MODEL> Specify the AI model to use
--api <API> For OpenAI models: 'chat' or 'responses' (default: responses for GPT-5)
-s, --stream Stream the response (partial support)
--thinking Show reasoning/thinking process (O3, O4, GPT-5 via Responses API)
--thinking Show reasoning/thinking process (GPT-5 via Responses API)
--verbose, -v Show detailed debug output
--config Show current configuration and exit
-h, --help Show this help message
@ -202,7 +202,7 @@ struct AICLI {
# Use specific models
ai-cli --model claude "Explain quantum computing"
ai-cli --model gpt-4o "Describe this image"
ai-cli --model gpt-5.5 "Describe this image"
ai-cli --model grok "Tell me a joke"
ai-cli --model llama3.3 "Help me debug this code"
@ -222,14 +222,12 @@ struct AICLI {
OpenAI:
gpt-5, gpt-5-pro, gpt-5-mini, gpt-5-nano (GPT-5 series, August 2025)
gpt-5-thinking, gpt-5-thinking-mini, gpt-5-thinking-nano
gpt-4.1, gpt-4.1-mini, o4-mini (GPT-4.1 / reasoning)
gpt-4o, gpt-4o-mini (Multimodal)
gpt-4-turbo (Legacy)
gpt-5.5, gpt-5.2, gpt-5.1 (flagship)
gpt-5.5, gpt-5-mini (multimodal)
Anthropic:
claude-opus-4-1-20250805, claude-sonnet-4-20250514 (Claude 4)
claude-3-7-sonnet (Claude 3.7)
claude-3-5-opus, claude-3-5-sonnet, claude-3-5-haiku (Claude 3.5)
claude-opus-4-7, claude-sonnet-4.5, claude-haiku-4.5 (Claude 4.x)
Google:
gemini-2.5-pro (reasoning, thinking support)
@ -257,7 +255,7 @@ struct AICLI {
SHORTCUTS:
claude, opus claude-opus-4-1-20250805
gpt, gpt4 gpt-4.1
gpt gpt-5.5
grok grok-4-fast-reasoning
gemini gemini-2.5-flash
llama, llama3 llama3.3
@ -473,7 +471,7 @@ struct AICLI {
let supportsThinking = self.isReasoningModel(model) && actualApiMode != .chat
if config.showThinking, !supportsThinking {
print("⚠️ Note: --thinking only works with O3, O4, and GPT-5 models via Responses API")
print("⚠️ Note: --thinking only works with GPT-5 models via Responses API")
}
if case let .openai(openaiModel) = model, actualApiMode == .chat {
@ -585,8 +583,7 @@ struct AICLI {
static func isReasoningModel(_ model: LanguageModel) -> Bool {
guard case let .openai(openaiModel) = model else { return false }
switch openaiModel {
case .o4Mini,
.gpt5,
case .gpt5,
.gpt5Pro,
.gpt5Mini,
.gpt5Nano,
@ -672,7 +669,7 @@ struct AICLI {
}
}
// If no summary, try content array (for O3/O4)
// If no summary, try content array.
if reasoningText == nil || reasoningText?.isEmpty == true {
if let contentArray = output["content"] as? [[String: Any]] {
let reasoningParts = contentArray.compactMap { item -> String? in
@ -801,15 +798,11 @@ struct AICLI {
switch model {
case let .openai(openaiModel):
switch openaiModel {
case .gpt5: return nil // Pricing TBD
case .gpt5Mini: return nil // Pricing TBD
case .gpt5Nano: return nil // Pricing TBD
case .gpt4o:
case .gpt55, .gpt52, .gpt51, .gpt5:
inputCostPer1k = 0.005
outputCostPer1k = 0.015
case .gpt4oMini:
inputCostPer1k = 0.000_15
outputCostPer1k = 0.0006
outputCostPer1k = 0.020
case .gpt5Mini, .gpt5Nano:
return nil // Pricing TBD
default: return nil
}
case let .anthropic(anthropicModel):

View File

@ -56,8 +56,8 @@ extension TachikomaExamples {
print("Creating provider-specific models with type safety:")
// OpenAI models
let gpt4o = Model.openai(.gpt4o)
let gpt41 = Model.openai(.gpt41)
let gpt55 = Model.openai(.gpt55)
let gpt52 = Model.openai(.gpt52)
let gpt5Mini = Model.openai(.gpt5Mini)
// Anthropic models
@ -75,11 +75,11 @@ extension TachikomaExamples {
// Custom endpoints
let openRouter = Model.openRouter(modelId: "anthropic/claude-sonnet-4.5")
let customAPI = Model.openaiCompatible(modelId: "gpt-4", baseURL: "https://api.azure.com")
let customAPI = Model.openaiCompatible(modelId: "gpt-5.5", baseURL: "https://api.azure.com")
let models = [
gpt4o,
gpt41,
gpt55,
gpt52,
gpt5Mini,
opus4,
sonnet4,
@ -100,7 +100,7 @@ extension TachikomaExamples {
// Model capabilities
print("\nModel capabilities:")
print(" • Vision support: \(gpt4o.supportsVision)")
print(" • Vision support: \(gpt55.supportsVision)")
print(" • Tool support: \(opus4.supportsTools)")
print(" • Streaming support: \(sonnet4.supportsStreaming)")
}
@ -192,7 +192,7 @@ extension TachikomaExamples {
// Record some usage
try? await tracker.recordUsage(
operation: .generation,
model: "gpt-4o".lowercased(),
model: "gpt-5.5".lowercased(),
inputTokens: 100,
outputTokens: 50,
cost: 0.003,
@ -273,7 +273,7 @@ extension TachikomaExamples {
// Test different provider creations (these will fail without API keys, which is expected)
let models: [(String, Model)] = [
("OpenAI GPT-4o", .openai(.gpt4o)),
("OpenAI GPT-5.5", .openai(.gpt55)),
("Anthropic Opus 4", .anthropic(.opus4)),
("Grok 4 Fast", .grok(.grok4FastReasoning)),
("Ollama Llama 3.3", .ollama(.llama3_3)),
@ -319,7 +319,7 @@ extension String {
extension Model {
var supportsVision: Bool {
switch self {
case .openai(.gpt4o), .grok(.grok2Vision), .ollama(.llava):
case .openai(.gpt55), .grok(.grok2Vision), .ollama(.llava):
true
default:
false

View File

@ -50,7 +50,7 @@ In interactive mode:
```bash
agent-cli --model gpt-5 "Explain quantum computing"
agent-cli --model claude "Write a haiku"
agent-cli --model o3 --thinking "Solve this step by step: ..."
agent-cli --model gpt-5-thinking --thinking "Solve this step by step: ..."
```
### With MCP Tools
@ -171,13 +171,11 @@ agent-cli --mcp-server "db -- npx @modelcontextprotocol/server-postgres postgres
## Supported Models
### OpenAI
- GPT-5 series: `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
- O-series: `o3`, `o3-mini`, `o3-pro`, `o4-mini`
- GPT-4: `gpt-4.1`, `gpt-4o`, `gpt-4-turbo`
- GPT-5 series: `gpt-5.5`, `gpt-5.2`, `gpt-5.1`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
### Anthropic
- Claude 4: `opus-4`, `sonnet-4`
- Claude 3.5: `claude`, `sonnet`, `haiku`
- Claude 4.x: `claude`, `sonnet`, `haiku`
### Others
- Google: `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
@ -209,7 +207,7 @@ agent-cli --mcp-server "db -- npx @modelcontextprotocol/server-sqlite ./app.db"
### Complex Reasoning
```bash
agent-cli --model o3 --thinking \
agent-cli --model gpt-5-thinking --thinking \
"Plan a distributed system architecture for a social media platform"
```

View File

@ -163,8 +163,7 @@ final class Agent {
switch model {
case let .openai(openaiModel):
switch openaiModel {
case .o4Mini,
.gpt5,
case .gpt5,
.gpt5Pro,
.gpt5Mini,
.gpt5Nano,

View File

@ -27,7 +27,7 @@ struct AgentCLI: AsyncParsableCommand {
@Argument(help: "Query or task for the agent")
var query: String?
@Option(name: .shortAndLong, help: "AI model to use (e.g., gpt-5, claude, o3)")
@Option(name: .shortAndLong, help: "AI model to use (e.g., gpt-5.5, gpt-5-thinking, claude)")
var model: String = "gpt-5"
@Flag(name: .shortAndLong, help: "Interactive conversation mode")

View File

@ -49,7 +49,7 @@ echo " • Stream token handling"
echo ""
echo "🔑 API Integration Examples (require valid API keys):"
echo " • OpenAI GPT-4o, GPT-4.1, o3 generation"
echo " • OpenAI GPT-5 generation"
echo " • Anthropic Claude Opus 4, Sonnet 4 generation"
echo " • Grok 4 and Grok 2 Vision models"
echo " • Ollama local models (llama3.3, llava)"
@ -70,7 +70,7 @@ echo "🚀 How to Use Tachikoma:"
echo "========================"
echo ""
echo "1. Basic Generation:"
echo ' let answer = try await generate("What is 2+2?", using: .openai(.gpt4o))'
echo ' let answer = try await generate("What is 2+2?", using: .openai(.gpt55))'
echo ""
echo "2. With Tools:"
echo ' @ToolKit'
@ -88,4 +88,4 @@ echo ' let response = try await conversation.continue(using: .anthropic(.opus4
echo ""
echo "🕷️ Tachikoma - Intelligent • Adaptable • Reliable"
echo " All examples completed successfully!"
echo " All examples completed successfully!"

View File

@ -9,7 +9,7 @@ func demonstrateMultiChannelResponse() async throws {
print("=== Multi-Channel Response Demo ===\n")
let result = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [
.user("Explain how recursion works in programming"),
],
@ -36,7 +36,7 @@ func demonstrateReasoningEffort() async throws {
// High effort for complex problem
print("High effort response:")
let complexResult = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [
.user("Design a distributed system for real-time collaboration"),
],
@ -50,7 +50,7 @@ func demonstrateReasoningEffort() async throws {
// Low effort for simple query
print("\nLow effort response:")
let simpleResult = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [
.user("What is the capital of Japan?"),
],
@ -82,7 +82,7 @@ func demonstrateRetryHandler() async throws {
print("Attempting API call...")
// Simulate a call that might fail
return try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [.user("Hello")],
)
},
@ -144,7 +144,7 @@ func demonstrateEnhancedTools() async throws {
}
let result = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [
.user("What's 25 * 4 and what's the weather in Tokyo?"),
],
@ -278,7 +278,7 @@ func demonstrateIntegratedFeatures() async throws {
do {
let result = try await retryHandler.execute {
try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [
.system("You are a helpful assistant. Use channels to organize your response."),
.user("Analyze the benefits of functional programming"),

View File

@ -16,7 +16,7 @@ public struct RealtimeAPIDemo {
// 1. Create configuration
print("\n1⃣ Creating Configuration...")
let config = SessionConfiguration(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .nova,
instructions: "You are a helpful voice assistant",
inputAudioFormat: .pcm16,
@ -108,7 +108,7 @@ public struct RealtimeAPIDemo {
Example:
let conversation = try RealtimeConversation(configuration: config)
try await conversation.start(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .nova
)
@ -118,7 +118,7 @@ public struct RealtimeAPIDemo {
/// Create a sample configuration for testing
public static func createSampleConfiguration() -> SessionConfiguration {
SessionConfiguration.voiceConversation(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .nova,
)
}
@ -127,7 +127,7 @@ public struct RealtimeAPIDemo {
public static func validateTypes() -> Bool {
// Test configuration creation
let config = self.createSampleConfiguration()
guard config.model == "gpt-4o-realtime-preview" else { return false }
guard config.model == "gpt-realtime" else { return false }
// Test VAD configuration
let vad = RealtimeTurnDetection.serverVAD

View File

@ -49,7 +49,7 @@ class RealtimeVoiceAssistant {
// Start the conversation
self.conversation = try await startRealtimeConversation(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .nova,
instructions: """
You are a helpful voice assistant. Keep responses concise and natural.
@ -151,7 +151,7 @@ class RealtimeDemo {
// Server VAD automatically detects when user starts/stops speaking
let conversation = try await startRealtimeConversation(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .alloy,
instructions: "You are a voice assistant with server-side voice activity detection",
)
@ -171,7 +171,7 @@ class RealtimeDemo {
// Configure for both text and audio responses
let conversation = try await startRealtimeConversation(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .shimmer,
)
@ -222,7 +222,7 @@ class RealtimeDemo {
}
let conversation = try await startRealtimeConversation(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .echo,
instructions: "You are a smart home assistant. Use the available tools to control devices.",
tools: [smartHomeTool],
@ -315,7 +315,7 @@ struct RealtimeVoiceView: View {
private func setupConversation() async {
do {
conversation = try await startRealtimeConversation(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .nova,
)

View File

@ -13,7 +13,7 @@ func testRealtimeConfiguration() async throws {
// Test 1: Session Configuration
print("\n1⃣ Testing Session Configuration:")
let voiceConfig = SessionConfiguration.voiceConversation(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .nova,
)
print(" ✅ Model: \(voiceConfig.model)")

View File

@ -24,7 +24,7 @@ class BasicVoiceAssistant: ObservableObject {
// Start conversation with voice
try await self.conversation?.start(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .nova,
instructions: "You are a helpful voice assistant. Keep responses concise.",
)
@ -307,7 +307,7 @@ class AudioStreamingExample {
func setupAudioStreaming(apiKey: String) async throws {
// Configure for audio streaming
var config = EnhancedSessionConfiguration(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .echo,
inputAudioFormat: .pcm16,
outputAudioFormat: .pcm16,
@ -371,7 +371,7 @@ class MultiTurnConversation {
func runConversation(apiKey: String) async throws {
// Configure for multi-turn dialogue
let config = EnhancedSessionConfiguration(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .fable,
instructions: """
You are a knowledgeable assistant engaged in a multi-turn conversation.
@ -547,7 +547,7 @@ class VoiceAssistantViewController: UIViewController {
self.conversation = try RealtimeConversation(configuration: config)
try await self.conversation?.start(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .shimmer,
)

View File

@ -29,7 +29,7 @@ class RealtimeVoiceAssistant {
// Start with voice configuration
try await conversation.start(
model: .gpt4oRealtime,
model: .custom("gpt-realtime"),
voice: .nova,
instructions: "You are a helpful, witty, and friendly AI assistant. Keep responses concise.",
)
@ -77,7 +77,7 @@ class RealtimeVoiceAssistant {
// Advanced configuration with all features
let config = SessionConfiguration(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .nova,
instructions: """
You are an expert AI assistant with deep knowledge across many domains.
@ -147,7 +147,7 @@ class RealtimeVoiceAssistant {
// Configuration with tools
let config = SessionConfiguration.withTools(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .nova,
tools: [
// Weather tool

View File

@ -1,15 +1,6 @@
{
"originHash" : "af7882d9f5dc7b555ad8239a69ab2881f08c29a54e8bc5c4a70f3482727b8abc",
"originHash" : "12a454cd38a6ae2519d652cc0872f7f18feb64690ce83d1507bae6db71c1841c",
"pins" : [
{
"identity" : "commander",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/Commander.git",
"state" : {
"revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b",
"version" : "0.2.2"
}
},
{
"identity" : "eventsource",
"kind" : "remoteSourceControl",

View File

@ -99,7 +99,7 @@ print(result.text)
Common picks:
- Anthropic: `claude-opus-4-5` (`LanguageModel.default`)
- OpenAI: `gpt-5.2` (flagship), `gpt-5` (coding/agents), `o4-mini` (reasoning), `gpt-4o` (vision)
- OpenAI: `gpt-5.5` (flagship), `gpt-5` (coding/agents), `gpt-5-thinking` (reasoning)
- Google: `gemini-3-flash`
- Grok: `grok-4-fast-reasoning`
- Local: `ollama/llama3.3`

View File

@ -575,7 +575,7 @@ struct TKProviderValidator {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
request.httpBody = try? JSONSerialization.data(withJSONObject: [
"model": "claude-3-haiku-20241022",
"model": "claude-haiku-4.5",
"max_tokens": 1,
"messages": [
["role": "user", "content": "ping"],

View File

@ -113,10 +113,10 @@ public enum EmbeddingModel: Sendable {
// Convert to LanguageModel for usage tracking
switch self {
case .openai:
.openai(.gpt4o) // Placeholder for tracking
.openai(.gpt55) // Placeholder for tracking
case .cohere, .voyage, .custom:
// Return a dummy model for tracking purposes
.openai(.gpt4o)
.openai(.gpt55)
}
}
}

View File

@ -670,7 +670,7 @@ public func analyze(
model
} else {
// Use a vision-capable model by default
.openai(.gpt4o)
.openai(.gpt55)
}
// Ensure the model supports vision

View File

@ -237,6 +237,7 @@ public final class ModelCapabilityRegistry: @unchecked Sendable {
excludedParameters: ["temperature", "topP", "frequencyPenalty", "presencePenalty"],
)
self.capabilities["openai:gpt-5.5"] = gpt5Capabilities
self.capabilities["openai:gpt-5.1"] = gpt5Capabilities
self.capabilities["openai:gpt-5.2"] = gpt5Capabilities
self.capabilities["openai:gpt-5"] = gpt5Capabilities
@ -248,36 +249,6 @@ public final class ModelCapabilityRegistry: @unchecked Sendable {
self.capabilities["openai:gpt-5-thinking-nano"] = gpt5Capabilities
self.capabilities["openai:gpt-5-chat-latest"] = gpt5Capabilities
// O4/GPT-5 reasoning models (fixed temperature, reasoning effort)
let reasoningCapabilities = ModelParameterCapabilities(
supportsTemperature: false,
supportsTopP: false,
supportedProviderOptions: .init(
supportsReasoningEffort: true,
supportsPreviousResponseId: true,
),
forcedTemperature: 1.0,
excludedParameters: ["temperature", "topP"],
)
self.capabilities["openai:o4"] = reasoningCapabilities
self.capabilities["openai:o4-mini"] = reasoningCapabilities
// Standard GPT-4 models
let gpt4Capabilities = ModelParameterCapabilities(
supportedProviderOptions: .init(
supportsParallelToolCalls: true,
supportsResponseFormat: true,
supportsLogprobs: true,
),
)
self.capabilities["openai:gpt-4o"] = gpt4Capabilities
self.capabilities["openai:gpt-4o-mini"] = gpt4Capabilities
self.capabilities["openai:gpt-4.1"] = gpt4Capabilities
self.capabilities["openai:gpt-4.1-mini"] = gpt4Capabilities
self.capabilities["openai:gpt-4-turbo"] = gpt4Capabilities
// Claude 4 models with thinking
let claude4Capabilities = ModelParameterCapabilities(
supportedProviderOptions: .init(
@ -286,6 +257,7 @@ public final class ModelCapabilityRegistry: @unchecked Sendable {
),
)
self.capabilities["anthropic:claude-opus-4-7"] = claude4Capabilities
self.capabilities["anthropic:claude-opus-4-5"] = claude4Capabilities
self.capabilities["anthropic:claude-opus-4-1-20250805"] = claude4Capabilities
self.capabilities["anthropic:claude-sonnet-4-20250514"] = claude4Capabilities

View File

@ -56,7 +56,7 @@ public struct OpenAIOptions: Sendable, Codable {
/// Verbosity level for GPT-5 models
public var verbosity: Verbosity?
/// Reasoning effort for O3/O4 models
/// Reasoning effort for GPT-5 models
public var reasoningEffort: ReasoningEffort?
/// Previous response ID for Responses API chaining

View File

@ -27,7 +27,7 @@ import Foundation
//
// ```swift
// // Simple generation
// let answer = try await generate("What is 2+2?", using: .openai(.gpt4o))
// let answer = try await generate("What is 2+2?", using: .openai(.gpt55))
//
// // Conversation management
// let conversation = Conversation()
@ -114,7 +114,7 @@ public enum API {
/// Model selection system
public enum Models {
/// Type-safe model selection
/// - `.openai(.gpt4o)`, `.anthropic(.opus4)`, `.grok(.grok4)`, `.ollama(.llama3_3)`
/// - `.openai(.gpt55)`, `.anthropic(.opus47)`, `.grok(.grok4)`, `.ollama(.llama3_3)`
public static let typed = "Provider-specific model enums"
/// Custom endpoints
@ -196,16 +196,16 @@ public enum API {
/// Migration guide from legacy API to modern API
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public enum MigrationGuide {
/// Legacy: `Tachikoma.shared.getModel("gpt-4").getResponse(request)`
/// Modern: `generate("Hello", using: .openai(.gpt4o))`
/// Legacy: `Tachikoma.shared.getModel("gpt-5.5").getResponse(request)`
/// Modern: `generate("Hello", using: .openai(.gpt55))`
public static let simpleGeneration = """
// OLD (deprecated)
let model = try await Tachikoma.shared.getModel("gpt-4")
let model = try await Tachikoma.shared.getModel("gpt-5.5")
let request = ModelRequest(messages: [.user(content: .text("Hello"))], settings: .default)
let response = try await model.getResponse(request: request)
// NEW (modern)
let response = try await generate("Hello", using: .openai(.gpt4o))
let response = try await generate("Hello", using: .openai(.gpt55))
"""
/// Legacy: Complex ModelRequest/ModelResponse handling

View File

@ -186,8 +186,9 @@ public enum OpenAIAPIMode: String, Sendable, CaseIterable {
public static func defaultMode(for model: LanguageModel.OpenAI) -> OpenAIAPIMode {
// Determine default API mode for a given model
switch model {
case .o4Mini, .gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt51, .gpt52:
.responses // Reasoning models and GPT-5 default to Responses API
case .gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt51, .gpt52, .gpt55,
.gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano, .gpt5ChatLatest:
.responses // GPT-5 defaults to Responses API
default:
.chat // All other models use Chat Completions API
}
@ -614,7 +615,7 @@ public enum ResponseChannel: String, Sendable, Codable, CaseIterable {
case final // Final answer to the user
}
/// Reasoning effort level for models that support it (o3, opus-4, etc.)
/// Reasoning effort level for models that support it (GPT-5 thinking, opus-4, etc.)
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public enum ReasoningEffort: String, Sendable, Codable, CaseIterable {
case low

View File

@ -31,8 +31,8 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
// MARK: - Provider Sub-Enums
public enum OpenAI: Sendable, Hashable, CaseIterable {
/// Latest models (2025)
case o4Mini
/// GPT-5.5 Series
case gpt55 // Flagship GPT-5.5
/// GPT-5.2 Series
case gpt52 // Flagship GPT-5.2
@ -50,25 +50,12 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
case gpt5ThinkingNano
case gpt5ChatLatest // Non-reasoning default chat deployment
// GPT-4.1 Series
case gpt41
case gpt41Mini
// GPT-4o Series (Multimodal)
case gpt4o
case gpt4oMini
case gpt4oRealtime // Realtime API support
// Legacy support
case gpt4Turbo
case gpt35Turbo
/// Fine-tuned models
case custom(String)
public static var allCases: [OpenAI] {
[
.o4Mini,
.gpt55,
.gpt52,
.gpt51,
.gpt5,
@ -79,20 +66,13 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
.gpt5ThinkingMini,
.gpt5ThinkingNano,
.gpt5ChatLatest,
.gpt41,
.gpt41Mini,
.gpt4o,
.gpt4oMini,
.gpt4oRealtime,
.gpt4Turbo,
.gpt35Turbo,
]
}
public var modelId: String {
switch self {
case let .custom(id): id
case .o4Mini: "o4-mini"
case .gpt55: "gpt-5.5"
case .gpt52: "gpt-5.2"
case .gpt51: "gpt-5.1"
case .gpt5: "gpt-5"
@ -103,83 +83,80 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
case .gpt5ThinkingMini: "gpt-5-thinking-mini"
case .gpt5ThinkingNano: "gpt-5-thinking-nano"
case .gpt5ChatLatest: "gpt-5-chat-latest"
case .gpt41: "gpt-4.1"
case .gpt41Mini: "gpt-4.1-mini"
case .gpt4o: "gpt-4o"
case .gpt4oMini: "gpt-4o-mini"
case .gpt4oRealtime: "gpt-4o-realtime-preview"
case .gpt4Turbo: "gpt-4-turbo"
case .gpt35Turbo: "gpt-3.5-turbo"
}
}
public var supportsVision: Bool {
switch self {
case .gpt52,
case .gpt55,
.gpt52,
.gpt51,
.gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano,
.gpt5ChatLatest: true // GPT-5+ supports multimodal
case .gpt4o, .gpt4oMini, .gpt4oRealtime: true
default: false
}
}
public var supportsTools: Bool {
switch self {
case .o4Mini: true
case .gpt52,
case .gpt55,
.gpt52,
.gpt51,
.gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano,
.gpt5ChatLatest: true // GPT-5+ excels at tool calling
case .gpt41, .gpt41Mini, .gpt4o, .gpt4oMini, .gpt4oRealtime, .gpt4Turbo: true
case .gpt35Turbo: true
case .custom: true // Assume custom models support tools
}
}
public var supportsAudioInput: Bool {
switch self {
case .gpt52,
case .gpt55,
.gpt52,
.gpt51,
.gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano,
.gpt5ChatLatest: true // GPT-5+ is fully multimodal
case .gpt4o, .gpt4oMini, .gpt4oRealtime: true // GPT-4o models support native audio input
default: false
}
}
public var supportsAudioOutput: Bool {
switch self {
case .gpt4oRealtime: true // Realtime API supports native audio output
case let .custom(id): id.contains("realtime")
default: false
}
}
public var supportsRealtime: Bool {
switch self {
case .gpt4oRealtime: true
case let .custom(id): id.contains("realtime")
default: false
}
}
public var contextLength: Int {
switch self {
case .o4Mini: 128_000
case .gpt52,
case .gpt55,
.gpt52,
.gpt51,
.gpt5, .gpt5Pro, .gpt5Mini, .gpt5Nano, .gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano,
.gpt5ChatLatest: 400_000 // 272k input + 128k output
case .gpt41, .gpt41Mini: 1_000_000
case .gpt4o, .gpt4oMini, .gpt4oRealtime: 128_000
case .gpt4Turbo: 128_000
case .gpt35Turbo: 16000
case .custom: 128_000 // Default assumption
}
}
public var isUnsupportedLegacyFamily: Bool {
let normalized = self.modelId.lowercased()
let compact = normalized.replacingOccurrences(of: "-", with: "")
.replacingOccurrences(of: ".", with: "")
return normalized.hasPrefix("gpt-4") || compact.hasPrefix("gpt4") ||
normalized.hasPrefix("gpt-3") || compact.hasPrefix("gpt3") ||
normalized.hasPrefix("o3") || normalized.hasPrefix("o4")
}
}
public enum Anthropic: Sendable, Hashable, CaseIterable {
// Claude 4.x / 4.5 Series (2025)
// Claude 4.x / 4.5+ Series
case opus47
case opus45
case opus4
case opus4Thinking
@ -193,6 +170,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
public static var allCases: [Anthropic] {
[
.opus47,
.opus45,
.opus4,
.opus4Thinking,
@ -206,6 +184,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
public var modelId: String {
switch self {
case let .custom(id): id
case .opus47: "claude-opus-4-7"
case .opus45: "claude-opus-4-5"
case .opus4: "claude-opus-4-1-20250805"
case .opus4Thinking: "claude-opus-4-1-20250805-thinking"
@ -218,7 +197,8 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
public var supportsVision: Bool {
switch self {
case .opus45, .opus4, .opus4Thinking, .sonnet4, .sonnet4Thinking, .sonnet45, .haiku45: true
case .opus47, .opus45, .opus4, .opus4Thinking, .sonnet4, .sonnet4Thinking, .sonnet45, .haiku45:
true
case .custom: true // Assume custom models support vision
}
}
@ -239,6 +219,7 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
public var contextLength: Int {
switch self {
case .opus47: 1_000_000
case .opus45, .opus4, .opus4Thinking, .sonnet4, .sonnet4Thinking, .sonnet45, .haiku45: 500_000
case .custom: 200_000 // Default assumption
}
@ -869,15 +850,12 @@ public enum LanguageModel: Sendable, CustomStringConvertible, Hashable {
// MARK: - Default Model
public static let `default`: LanguageModel = .anthropic(.opus45)
public static let `default`: LanguageModel = .anthropic(.opus47)
// MARK: - Convenience Static Properties
/// Default Claude model (opus45)
public static let claude: LanguageModel = .anthropic(.opus45)
/// Default GPT-4o model
public static let gpt4o: LanguageModel = .openai(.gpt4o)
/// Default Claude model (opus47)
public static let claude: LanguageModel = .anthropic(.opus47)
/// Default Grok model (Grok-4-0709)
public static let grok4: LanguageModel = .grok(.grok4)
@ -1137,6 +1115,10 @@ extension LanguageModel {
// MARK: OpenAI models
if compact.contains("gpt4") || compact.contains("gpt3") || compact.contains("o3") || compact.contains("o4") {
return nil
}
if dashed == "gpt-5-pro" || compact == "gpt5pro" {
return .openai(.gpt5Pro)
}
@ -1151,6 +1133,13 @@ extension LanguageModel {
return .openai(.gpt5Thinking)
}
if dotted.contains("gpt-5-5") || compact.contains("gpt55") {
// GPT-5.5 currently has no mini/nano variants; map those suffixes to GPT-5 mini/nano.
if dotted.contains("nano") || compact.contains("nano") { return .openai(.gpt5Nano) }
if dotted.contains("mini") || compact.contains("mini") { return .openai(.gpt5Mini) }
return .openai(.gpt55)
}
if dotted.contains("gpt-5-2") || compact.contains("gpt52") {
// GPT-5.2 currently has no mini/nano variants; map those suffixes to GPT-5 mini/nano.
if dotted.contains("nano") || compact.contains("nano") { return .openai(.gpt5Nano) }
@ -1181,40 +1170,23 @@ extension LanguageModel {
return .openai(.gpt5)
}
if dotted.contains("gpt-4o-realtime") || compact.contains("gpt4orealtime") {
return .openai(.gpt4oRealtime)
}
// MARK: Anthropic models
if dotted.contains("gpt-4o-mini") || compact.contains("gpt4omini") {
return .openai(.gpt4oMini)
}
if dotted.contains("gpt-4o") || compact.contains("gpt4o") {
return .openai(.gpt4o)
}
if dotted.contains("gpt-4.1-mini") || compact.contains("gpt41mini") {
return .openai(.gpt41Mini)
}
if dotted.contains("gpt-4.1") || compact.contains("gpt41") {
return .openai(.gpt41)
if dotted.contains("claude-3") || compact.contains("claude3") {
return nil
}
if
dashed == "o3" || compact == "o3" || dashed == "o3-pro" || dashed == "o3-mini" || compact == "o3mini" ||
compact == "o3pro"
dotted.contains("claude-opus-4-7") ||
dotted.contains("claude-opus-4.7") ||
compact.contains("claudeopus47") ||
dotted.contains("opus-4-7") ||
dotted.contains("opus-4.7") ||
compact.contains("opus47")
{
// o3 family is deprecated; steer callers to GPT-5.1 Mini
return .openai(.gpt5Mini)
return .anthropic(.opus47)
}
if dashed == "o4-mini" || compact == "o4mini" {
return .openai(.o4Mini)
}
// MARK: Anthropic models
if
dotted.contains("claude-opus-4-5") ||
dotted.contains("claude-opus-4.5") ||
@ -1268,7 +1240,7 @@ extension LanguageModel {
let canonicalForms = [normalized, dashed, compact]
if canonicalForms.contains(where: { genericClaudeIdentifiers.contains($0) }) {
return .anthropic(.sonnet45)
return .anthropic(.opus47)
}
// MARK: Google models
@ -1385,10 +1357,6 @@ extension LanguageModel {
return .openai(.gpt5Mini)
}
if compact.contains("o4") {
return .openai(.o4Mini)
}
return nil
}
}

View File

@ -88,7 +88,7 @@ public struct ModelSettings: Sendable, Codable {
/// Legacy initializer for backward compatibility
public init(
modelName: String = "gpt-4",
modelName: String = "gpt-5.5",
maxTokens: Int? = nil,
temperature: Double? = nil,
topP: Double? = nil,

View File

@ -35,6 +35,10 @@ public struct ModelSelector {
return .grok(grokModel)
}
if Self.isUnsupportedLegacyOpenAIModel(normalized) || Self.isUnsupportedLegacyAnthropicModel(normalized) {
throw ModelValidationError.unsupportedModel(modelString)
}
// Ollama shortcuts and models
if let ollamaModel = parseOllamaModel(normalized) {
return .ollama(ollamaModel)
@ -46,7 +50,7 @@ public struct ModelSelector {
}
// Custom model ID - try to infer provider
if normalized.contains("gpt") || normalized.contains("o3") || normalized.contains("o4") {
if normalized.contains("gpt-5") || normalized.contains("gpt5") {
return .openai(.custom(normalized))
}
@ -66,6 +70,13 @@ public struct ModelSelector {
private static func parseOpenAIModel(_ input: String) -> Model.OpenAI? {
switch input {
// GPT-5.5 models
case "gpt-5.5", "gpt5.5", "gpt-5-5", "gpt5-5", "gpt55":
return .gpt55
case "gpt-5.5-mini", "gpt5.5-mini", "gpt55-mini", "gpt55mini", "gpt-5-5-mini", "gpt5-5-mini":
return .gpt5Mini
case "gpt-5.5-nano", "gpt5.5-nano", "gpt55-nano", "gpt55nano", "gpt-5-5-nano", "gpt5-5-nano":
return .gpt5Nano
// GPT-5.2 models
case "gpt-5.2", "gpt5.2", "gpt-5-2", "gpt5-2", "gpt52":
return .gpt52
@ -97,27 +108,17 @@ public struct ModelSelector {
return .gpt5ThinkingNano
case "gpt-5-chat-latest", "gpt5-chat-latest":
return .gpt5ChatLatest
// Direct matches
case "gpt-4o", "gpt4o":
return .gpt4o
case "gpt-4o-mini", "gpt4o-mini", "gpt4omini":
return .gpt4oMini
case "gpt-4.1", "gpt4.1", "gpt41":
return .gpt41
case "gpt-4.1-mini", "gpt4.1-mini", "gpt41mini":
return .gpt41Mini
case "o4-mini", "o4mini":
return .o4Mini
// Shortcuts
case "gpt":
return .gpt51 // Default to flagship GPT-5.1
case "gpt4", "gpt-4":
return .gpt4o // Default to latest GPT-4 variant
return .gpt55 // Default to flagship GPT-5.5
case "openai":
return .gpt51 // Default to GPT-5.1
return .gpt55 // Default to GPT-5.5
default:
// Check if it's an OpenAI model ID
if input.hasPrefix("gpt") || input.hasPrefix("o4") {
if Self.isUnsupportedLegacyOpenAIModel(input) {
return nil
}
if input.hasPrefix("gpt-5") || input.hasPrefix("gpt5") {
return .custom(input)
}
return nil
@ -131,6 +132,8 @@ public struct ModelSelector {
return .opus4
case "claude-opus-4-20250514-thinking":
return .opus4Thinking
case "claude-opus-4-7", "claude-opus-4.7", "opus-4-7", "opus-4.7", "opus47":
return .opus47
case "claude-opus-4-5", "claude-opus-4.5", "opus-4-5", "opus-4.5", "opus45":
return .opus45
case "claude-sonnet-4-20250514":
@ -141,17 +144,20 @@ public struct ModelSelector {
return .sonnet45
// Shortcuts
case "claude":
return .sonnet45 // Default plain Claude alias to latest Sonnet
return .opus47
case "claude-opus", "opus":
return .opus45
return .opus47
case "claude-sonnet", "sonnet":
return .sonnet4
case "claude-haiku", "haiku":
return .haiku45
case "anthropic":
return .opus45 // Default Anthropic model
return .opus47 // Default Anthropic model
default:
// Check if it's a Claude model ID
if Self.isUnsupportedLegacyAnthropicModel(input) {
return nil
}
if input.hasPrefix("claude") {
return .custom(input)
}
@ -159,6 +165,20 @@ public struct ModelSelector {
}
}
private static func isUnsupportedLegacyOpenAIModel(_ input: String) -> Bool {
let normalized = input.lowercased()
let compact = normalized.replacingOccurrences(of: "-", with: "").replacingOccurrences(of: ".", with: "")
return normalized.hasPrefix("gpt-4") || compact.hasPrefix("gpt4") ||
normalized.hasPrefix("gpt-3") || compact.hasPrefix("gpt3") ||
normalized.hasPrefix("o3") || normalized.hasPrefix("o4")
}
private static func isUnsupportedLegacyAnthropicModel(_ input: String) -> Bool {
let normalized = input.lowercased()
let compact = normalized.replacingOccurrences(of: "-", with: "").replacingOccurrences(of: ".", with: "")
return normalized.hasPrefix("claude-3") || compact.hasPrefix("claude3")
}
private static func parseGoogleModel(_ input: String) -> Model.Google? {
switch input {
case "gemini-3-flash", "gemini-3-flash-preview", "gemini3flash", "gemini-3flash":
@ -388,14 +408,14 @@ public func getAllAvailableModels() -> String {
)
output += "\nShortcuts:\n"
output += " • claude, claude-opus, opus → claude-opus-4-20250514\n"
output += " • gpt, gpt4 → gpt-4.1\n"
output += " • claude, claude-opus, opus → claude-opus-4-7\n"
output += " • gpt → gpt-5.5\n"
output += " • gemini → gemini-3-flash\n"
output += " • grok → grok-4-fast-reasoning\n"
output += " • llama, llama3 → llama3.3\n"
output += "\nCustom Models:\n"
output += " • OpenRouter: anthropic/claude-3.5-sonnet\n"
output += " • OpenRouter: anthropic/claude-opus-4-7\n"
output += " • Custom OpenAI: custom-gpt-model\n"
output += " • Local Ollama: any-model:tag\n"
@ -423,15 +443,15 @@ extension ModelSelector {
// Get recommended models for specific use cases
switch useCase {
case .coding:
[.claude, .gpt4o, .google(.gemini25Pro)]
[.claude, .openai(.gpt55), .google(.gemini25Pro)]
case .vision:
[.claude, .gpt4o, .google(.gemini3Flash)]
[.claude, .openai(.gpt55), .google(.gemini3Flash)]
case .reasoning:
[.openai(.gpt5Mini), .claude, .google(.gemini25Pro)]
case .local:
[.llama, .ollama(.mistralNemo), .ollama(.commandRPlus)]
case .general:
[.claude, .gpt4o, .google(.gemini3Flash), .grok(.grok4FastReasoning), .llama]
[.claude, .openai(.gpt55), .google(.gemini3Flash), .grok(.grok4FastReasoning), .llama]
}
}
}
@ -449,6 +469,7 @@ public enum UseCase {
public enum ModelValidationError: Error, LocalizedError {
case visionNotSupported(String)
case toolsNotSupported(String)
case unsupportedModel(String)
public var errorDescription: String? {
switch self {
@ -456,6 +477,8 @@ public enum ModelValidationError: Error, LocalizedError {
"Model '\(modelId)' does not support vision inputs"
case let .toolsNotSupported(modelId):
"Model '\(modelId)' does not support tool calling"
case let .unsupportedModel(modelId):
"Model '\(modelId)' is no longer supported"
}
}
}

View File

@ -20,6 +20,10 @@ public final class OpenAIProvider: ModelProvider {
configuration: TachikomaConfiguration,
session: URLSession = .shared,
) throws {
guard !model.isUnsupportedLegacyFamily else {
throw TachikomaError.unsupportedOperation("OpenAI model '\(model.modelId)' is no longer supported")
}
self.model = model
self.modelId = model.modelId
self.baseURL = configuration.getBaseURL(for: .openai) ?? "https://api.openai.com/v1"

View File

@ -3,7 +3,7 @@ import Foundation
import FoundationNetworking
#endif
/// Provider for OpenAI Responses API (GPT-5, o3, o4)
/// Provider for OpenAI Responses API (GPT-5)
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public final class OpenAIResponsesProvider: ModelProvider {
public let modelId: String
@ -29,6 +29,10 @@ public final class OpenAIResponsesProvider: ModelProvider {
configuration: TachikomaConfiguration,
session: URLSession = .shared,
) throws {
guard !model.isUnsupportedLegacyFamily else {
throw TachikomaError.unsupportedOperation("OpenAI model '\(model.modelId)' is no longer supported")
}
self.model = model
self.modelId = model.modelId
self.configuration = configuration
@ -918,7 +922,7 @@ public final class OpenAIResponsesProvider: ModelProvider {
private static func isReasoningModel(_ model: LanguageModel.OpenAI) -> Bool {
switch model {
case .o4Mini:
case .gpt5Thinking, .gpt5ThinkingMini, .gpt5ThinkingNano:
true
default:
false
@ -927,7 +931,8 @@ public final class OpenAIResponsesProvider: ModelProvider {
private static func isGPT5Model(_ model: LanguageModel.OpenAI) -> Bool {
switch model {
case .gpt52,
case .gpt55,
.gpt52,
.gpt51,
.gpt5,
.gpt5Pro,

View File

@ -28,7 +28,7 @@ struct OpenAIResponsesRequest: Encodable {
let serviceTier: String?
let include: [String]?
/// Reasoning configuration (for o3/o4/GPT-5)
/// Reasoning configuration (for GPT-5 thinking models)
let reasoning: ReasoningConfig?
/// Truncation for long inputs

View File

@ -15,9 +15,9 @@ public struct ProviderFactory {
// Create a provider for the specified language model
switch model {
case let .openai(openaiModel):
// Use Responses API for reasoning models (o4) and GPT-5 family
// Use Responses API for the GPT-5 family
switch openaiModel {
case .o4Mini,
case .gpt55,
.gpt52,
.gpt51,
.gpt5,

View File

@ -8,10 +8,10 @@ public enum ProviderParser {
/// The provider name (e.g., "openai", "anthropic", "ollama")
public let provider: String
/// The model name (e.g., "gpt-4", "claude-3", "llava:latest")
/// The model name (e.g., "gpt-5.5", "claude-opus-4-7", "llava:latest")
public let model: String
/// The full string representation (e.g., "openai/gpt-4")
/// The full string representation (e.g., "openai/gpt-5.5")
public var fullString: String {
"\(self.provider)/\(self.model)"
}
@ -23,7 +23,7 @@ public enum ProviderParser {
}
/// Parse a provider string in the format "provider/model"
/// - Parameter providerString: String like "openai/gpt-4" or "ollama/llava:latest"
/// - Parameter providerString: String like "openai/gpt-5.5" or "ollama/llava:latest"
/// - Returns: Parsed configuration or nil if invalid format
public static func parse(_ providerString: String) -> ProviderConfig? {
// Parse a provider string in the format "provider/model"
@ -44,7 +44,7 @@ public enum ProviderParser {
}
/// Parse a comma-separated list of providers
/// - Parameter providersString: String like "openai/gpt-4,anthropic/claude-3,ollama/llava:latest"
/// - Parameter providersString: String like "openai/gpt-5.5,anthropic/claude-opus-4-7,ollama/llava:latest"
/// - Returns: Array of parsed configurations
public static func parseList(_ providersString: String) -> [ProviderConfig] {
// Parse a comma-separated list of providers
@ -54,7 +54,7 @@ public enum ProviderParser {
}
/// Get the first provider from a comma-separated list
/// - Parameter providersString: String like "openai/gpt-4,anthropic/claude-3"
/// - Parameter providersString: String like "openai/gpt-5.5,anthropic/claude-opus-4-7"
/// - Returns: First parsed configuration or nil if none valid
public static func parseFirst(_ providersString: String) -> ProviderConfig? {
// Get the first provider from a comma-separated list
@ -202,8 +202,22 @@ public enum ProviderParser {
// MARK: - Private Helpers
private static func parseOpenAIModel(_ modelString: String) -> LanguageModel? {
switch modelString.lowercased() {
case "o4-mini": .openai(.o4Mini)
let normalized = modelString.lowercased()
let compact = normalized.replacingOccurrences(of: "-", with: "").replacingOccurrences(of: ".", with: "")
guard
!normalized.hasPrefix("gpt-4"), !compact.hasPrefix("gpt4"),
!normalized.hasPrefix("gpt-3"), !compact.hasPrefix("gpt3"),
!normalized.hasPrefix("o3"), !normalized.hasPrefix("o4")
else {
return nil
}
return switch normalized {
case "gpt-5.5", "gpt5.5", "gpt-5-5", "gpt5-5", "gpt55": .openai(.gpt55)
case "gpt-5.5-mini", "gpt5.5-mini", "gpt-5-5-mini", "gpt5-5-mini", "gpt55-mini", "gpt55mini":
.openai(.gpt5Mini)
case "gpt-5.5-nano", "gpt5.5-nano", "gpt-5-5-nano", "gpt5-5-nano", "gpt55-nano", "gpt55nano":
.openai(.gpt5Nano)
case "gpt-5.2", "gpt5.2", "gpt-5-2", "gpt5-2", "gpt52": .openai(.gpt52)
case "gpt-5.2-mini", "gpt5.2-mini", "gpt-5-2-mini", "gpt5-2-mini", "gpt52-mini", "gpt52mini":
.openai(.gpt5Mini)
@ -222,12 +236,6 @@ public enum ProviderParser {
case "gpt-5-thinking-mini", "gpt5-thinking-mini", "gpt5thinkingmini": .openai(.gpt5ThinkingMini)
case "gpt-5-thinking-nano", "gpt5-thinking-nano", "gpt5thinkingnano": .openai(.gpt5ThinkingNano)
case "gpt-5-chat-latest", "gpt5-chat-latest": .openai(.gpt5ChatLatest)
case "gpt-4.1", "gpt4.1": .openai(.gpt41)
case "gpt-4.1-mini", "gpt4.1-mini": .openai(.gpt41Mini)
case "gpt-4o", "gpt4o": .openai(.gpt4o)
case "gpt-4o-mini", "gpt4o-mini": .openai(.gpt4oMini)
case "gpt-4-turbo", "gpt4-turbo": .openai(.gpt4Turbo)
case "gpt-3.5-turbo", "gpt35-turbo": .openai(.gpt35Turbo)
default:
// Handle custom/fine-tuned models
.openai(.custom(modelString))
@ -235,7 +243,15 @@ public enum ProviderParser {
}
private static func parseAnthropicModel(_ modelString: String) -> LanguageModel? {
switch modelString.lowercased() {
let normalized = modelString.lowercased()
let compact = normalized.replacingOccurrences(of: "-", with: "").replacingOccurrences(of: ".", with: "")
guard !normalized.hasPrefix("claude-3"), !compact.hasPrefix("claude3") else {
return nil
}
return switch normalized {
case "claude-opus-4-7", "claude-opus-4.7", "claude-opus-4-7-latest", "opus-4-7", "opus-4.7", "opus47":
.anthropic(.opus47)
case "claude-opus-4-5", "claude-opus-4.5", "claude-opus-4-5-latest", "opus-4-5", "opus-4.5", "opus45":
.anthropic(.opus45)
case "claude-opus-4-1-20250805", "claude-opus-4-20250514", "claude-opus-4", "opus-4": .anthropic(.opus4)

View File

@ -528,7 +528,7 @@ public struct ModelCostCalculator: Sendable {
// OpenAI Pricing (as of 2025)
case let .openai(openaiModel):
switch openaiModel {
case .o4Mini: (1.50, 6.00)
case .gpt55: (5.00, 20.00) // GPT-5.5 pricing estimate
case .gpt52: (5.00, 20.00) // GPT-5.2 pricing estimate
case .gpt51: (5.00, 20.00) // GPT-5.1 pricing estimate
case .gpt5: (5.00, 20.00) // GPT-5 pricing estimate
@ -539,18 +539,12 @@ public struct ModelCostCalculator: Sendable {
case .gpt5ThinkingMini: (4.00, 16.00)
case .gpt5ThinkingNano: (1.50, 6.00)
case .gpt5ChatLatest: (2.50, 10.00)
case .gpt41: (2.50, 10.00)
case .gpt41Mini: (0.15, 0.60)
case .gpt4o: (2.50, 10.00)
case .gpt4oMini: (0.15, 0.60)
case .gpt4oRealtime: (5.00, 20.00) // Realtime API pricing estimate
case .gpt4Turbo: (10.00, 30.00)
case .gpt35Turbo: (0.50, 1.50)
case .custom: (2.50, 10.00) // Default estimate
}
// Anthropic Pricing (as of 2025)
case let .anthropic(anthropicModel):
switch anthropicModel {
case .opus47: (15.00, 75.00)
case .opus45: (5.00, 25.00)
case .opus4, .opus4Thinking: (15.00, 75.00)
case .sonnet4, .sonnet4Thinking: (3.00, 15.00)

View File

@ -128,7 +128,7 @@ public struct InputAudioTranscription: Sendable, Codable {
/// Session configuration with all options
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public struct SessionConfiguration: Sendable, Codable {
/// Model to use (e.g., "gpt-4o-realtime-preview")
/// Model to use (e.g., "gpt-realtime")
public var model: String
/// Voice for audio responses
@ -213,7 +213,7 @@ public struct SessionConfiguration: Sendable, Codable {
}
public init(
model: String = "gpt-4o-realtime-preview",
model: String = "gpt-realtime",
voice: RealtimeVoice = .alloy,
instructions: String? = nil,
inputAudioFormat: RealtimeAudioFormat = .pcm16,
@ -242,7 +242,7 @@ public struct SessionConfiguration: Sendable, Codable {
/// Create a default configuration for voice conversations
public static func voiceConversation(
model: String = "gpt-4o-realtime-preview",
model: String = "gpt-realtime",
voice: RealtimeVoice = .alloy,
)
-> SessionConfiguration
@ -258,7 +258,7 @@ public struct SessionConfiguration: Sendable, Codable {
/// Create a configuration for text-only interactions
public static func textOnly(
model: String = "gpt-4o-realtime-preview",
model: String = "gpt-realtime",
)
-> SessionConfiguration
{
@ -273,7 +273,7 @@ public struct SessionConfiguration: Sendable, Codable {
/// Create a configuration with tools
public static func withTools(
model: String = "gpt-4o-realtime-preview",
model: String = "gpt-realtime",
voice: RealtimeVoice = .alloy,
tools: [RealtimeTool],
)

View File

@ -150,7 +150,7 @@ public final class RealtimeConversation: ObservableObject {
// Create session with configuration
let sessionConfig = SessionConfiguration(
model: "gpt-4o-realtime-preview",
model: "gpt-realtime",
voice: .alloy,
instructions: nil,
tools: nil,
@ -167,7 +167,7 @@ public final class RealtimeConversation: ObservableObject {
/// Start the conversation
public func start(
model: LanguageModel.OpenAI = .gpt4oRealtime,
model: LanguageModel.OpenAI = .custom("gpt-realtime"),
voice: RealtimeVoice = .alloy,
instructions: String? = nil,
tools: [RealtimeTool]? = nil,
@ -495,7 +495,7 @@ public final class RealtimeConversation: ObservableObject {
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func startRealtimeConversation(
model: LanguageModel.OpenAI = .gpt4oRealtime,
model: LanguageModel.OpenAI = .custom("gpt-realtime"),
voice: RealtimeVoice = .alloy,
instructions: String? = nil,
tools: [AgentTool]? = nil,
@ -548,7 +548,7 @@ public final class RealtimeConversation {
}
public func start(
model _: LanguageModel.OpenAI = .gpt4oRealtime,
model _: LanguageModel.OpenAI = .custom("gpt-realtime"),
voice _: RealtimeVoice = .alloy,
instructions _: String? = nil,
tools _: [RealtimeTool]? = nil,
@ -560,7 +560,7 @@ public final class RealtimeConversation {
// swiftformat:disable wrapMultilineStatementBraces wrapReturnType indent
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func startRealtimeConversation(
model _: LanguageModel.OpenAI = .gpt4oRealtime,
model _: LanguageModel.OpenAI = .custom("gpt-realtime"),
voice _: RealtimeVoice = .alloy,
instructions _: String? = nil,
tools _: [AgentTool]? = nil,

View File

@ -188,7 +188,7 @@ public struct RealtimeSessionConfig: Codable, Sendable {
}
public init(
model: String = "gpt-4o-realtime-preview",
model: String = "gpt-realtime",
voice: RealtimeVoice = .alloy,
instructions: String? = nil,
tools: [RealtimeTool]? = nil,

View File

@ -16,20 +16,16 @@ public enum TranscriptionModel: Sendable, CustomStringConvertible {
public enum OpenAI: String, CaseIterable, Sendable {
case whisper1 = "whisper-1"
case gpt4oTranscribe = "gpt-4o-transcribe"
case gpt4oMiniTranscribe = "gpt-4o-mini-transcribe"
public var supportsTimestamps: Bool {
switch self {
case .whisper1: true
case .gpt4oTranscribe, .gpt4oMiniTranscribe: false
}
}
public var supportsLanguageDetection: Bool {
switch self {
case .whisper1: true
case .gpt4oTranscribe, .gpt4oMiniTranscribe: false
}
}
}
@ -155,12 +151,10 @@ public enum SpeechModel: Sendable, CustomStringConvertible {
public enum OpenAI: String, CaseIterable, Sendable {
case tts1 = "tts-1"
case tts1HD = "tts-1-hd"
case gpt4oMiniTTS = "gpt-4o-mini-tts"
public var supportsVoiceInstructions: Bool {
switch self {
case .tts1, .tts1HD: false
case .gpt4oMiniTTS: true
}
}
@ -226,5 +220,5 @@ public enum SpeechModel: Sendable, CustomStringConvertible {
public static let `default`: SpeechModel = .openai(.tts1)
public static let highQuality: SpeechModel = .openai(.tts1HD)
public static let fast: SpeechModel = .openai(.tts1)
public static let expressive: SpeechModel = .openai(.gpt4oMiniTTS)
public static let expressive: SpeechModel = .openai(.tts1HD)
}

View File

@ -12,7 +12,7 @@ import Tachikoma
// Use Tachikoma normally without MCP
let result = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: messages,
tools: staticTools
)
@ -26,7 +26,7 @@ import TachikomaMCP
// Now you can use MCP tools alongside static tools
let mcpTools = try await MCPToolDiscovery.withFilesystem()
let result = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: messages,
tools: staticTools + mcpTools
)

View File

@ -17,7 +17,7 @@ struct AsyncSequenceTests {
let result = StreamTextResult(
stream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -86,7 +86,7 @@ struct AsyncSequenceTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestData.self,
)
@ -117,7 +117,7 @@ struct AsyncSequenceTests {
let result = StreamTextResult(
stream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -149,7 +149,7 @@ struct AsyncSequenceTests {
let result = StreamTextResult(
stream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -183,7 +183,7 @@ struct AsyncSequenceTests {
let result = StreamTextResult(
stream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -218,7 +218,7 @@ struct AsyncSequenceTests {
let result = StreamTextResult(
stream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -262,7 +262,7 @@ struct AsyncSequenceTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestItem.self,
)

View File

@ -55,7 +55,7 @@ struct ConfigurationArchitectureTests {
// Example 1: Zero configuration
_ = {
Task {
_ = try await generate("What is 2+2?", using: .openai(.gpt4o))
_ = try await generate("What is 2+2?", using: .openai(.gpt55))
}
}
@ -66,7 +66,7 @@ struct ConfigurationArchitectureTests {
)
Task {
_ = try await generate("Hello", using: .openai(.gpt4o))
_ = try await generate("Hello", using: .openai(.gpt55))
}
}
@ -79,7 +79,7 @@ struct ConfigurationArchitectureTests {
_ = try await generate(
"Test prompt",
using: .openai(.gpt4o),
using: .openai(.gpt55),
configuration: testConfig,
)
}

View File

@ -10,7 +10,7 @@ struct GenerationTests {
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
let result = try await generate(
"What is 2+2?",
using: .openai(.gpt4o),
using: .openai(.gpt55),
maxTokens: 100,
configuration: config,
)
@ -18,7 +18,7 @@ struct GenerationTests {
self.assertOpenAIResult(
result,
prompt: "What is 2+2?",
modelId: "gpt-4o",
modelId: "gpt-5.5",
configuration: config,
)
}
@ -56,7 +56,7 @@ struct GenerationTests {
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
let result = try await generate(
"Tell me a joke",
using: .openai(.gpt4oMini),
using: .openai(.gpt5Mini),
system: "You are a comedian",
temperature: 0.8,
configuration: config,
@ -77,7 +77,7 @@ struct GenerationTests {
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
let stream = try await stream(
"Count to 5",
using: .openai(.gpt4o),
using: .openai(.gpt55),
maxTokens: 50,
configuration: config,
)
@ -141,7 +141,7 @@ struct GenerationTests {
let result = try await analyze(
image: .base64(testImageBase64),
prompt: "What do you see?",
using: .openai(.gpt4o),
using: .openai(.gpt55),
configuration: config,
)
@ -156,12 +156,12 @@ struct GenerationTests {
@Test
func `Analyze Function - Non-Vision Model Error`() async {
_ = await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
// GPT-4.1 doesn't support vision
// Custom OpenAI models default to text-only capabilities
await #expect(throws: TachikomaError.self) {
try await analyze(
image: .base64("test-image"),
prompt: "Describe this",
using: .openai(.gpt41),
using: .openai(.custom("text-only-openai")),
configuration: config,
)
}
@ -179,7 +179,7 @@ struct GenerationTests {
configuration: config,
)
// Should default to GPT-4o for vision tasks
// Should default to GPT-5.5 for vision tasks
self.assertOpenAIResult(
result,
prompt: "Analyze this image",
@ -194,7 +194,7 @@ struct GenerationTests {
func `Generate Function - Missing API Key`() async {
_ = await TestHelpers.withEmptyTestConfiguration { config in
await #expect(throws: TachikomaError.self) {
try await generate("Test", using: .openai(.gpt4o), configuration: config)
try await generate("Test", using: .openai(.gpt55), configuration: config)
}
}
}
@ -208,7 +208,7 @@ struct GenerationTests {
// With mock provider (test-key), this should work even with invalid URL
// Real implementations would fail with network error
do {
let result = try await generate("Test", using: .openai(.gpt4o), configuration: config)
let result = try await generate("Test", using: .openai(.gpt55), configuration: config)
#expect(!result.isEmpty)
} catch {
// If using real provider, invalid URL will cause network error
@ -226,7 +226,7 @@ struct GenerationTests {
// Test generation without tools
let result = try await generate(
"Hello",
using: .openai(.gpt4o),
using: .openai(.gpt55),
configuration: config,
)

View File

@ -68,8 +68,8 @@ struct LanguageModelCoverageTests {
@Test
func `LanguageModel top level switches`() {
let baseModels: [LanguageModel] = [
.openai(.gpt51),
.anthropic(.opus45),
.openai(.gpt55),
.anthropic(.opus47),
.google(.gemini25Flash),
.mistral(.large2),
.groq(.mixtral8x7b),

View File

@ -9,8 +9,8 @@ struct MinimalModernAPITests {
@Test
func `Model enum construction`() {
// Test that model enums can be constructed
let openaiModel = Model.openai(.gpt4o)
let anthropicModel = Model.anthropic(.opus45)
let openaiModel = Model.openai(.gpt55)
let anthropicModel = Model.anthropic(.opus47)
_ = Model.grok(.grok4)
_ = Model.ollama(.llama33)
@ -35,10 +35,10 @@ struct MinimalModernAPITests {
let defaultModel = Model.default
// Should compile without errors
switch defaultModel {
case .anthropic(.opus45):
case .anthropic(.opus47):
break // Expected default
default:
Issue.record("Expected default to be Anthropic Opus 4.5")
Issue.record("Expected default to be Anthropic Opus 4.7")
}
}

View File

@ -7,6 +7,7 @@ enum ModelCapabilitiesTests {
@Test
func `GPT-5 models exclude temperature and topP`() {
let models: [LanguageModel] = [
.openai(.gpt55),
.openai(.gpt52),
.openai(.gpt51),
.openai(.gpt5),
@ -35,26 +36,9 @@ enum ModelCapabilitiesTests {
}
@Test
func `Reasoning models have forced temperature`() {
let model = LanguageModel.openai(.o4Mini)
let capabilities = ModelCapabilityRegistry.shared.capabilities(for: model)
#expect(!capabilities.supportsTemperature)
#expect(!capabilities.supportsTopP)
#expect(capabilities.forcedTemperature == 1.0)
#expect(capabilities.excludedParameters.contains("temperature"))
#expect(capabilities.excludedParameters.contains("topP"))
#expect(capabilities.supportedProviderOptions.supportsReasoningEffort)
#expect(capabilities.supportedProviderOptions.supportsPreviousResponseId)
}
@Test
func `GPT-4 models support standard parameters`() {
func `Custom OpenAI models support standard parameters`() {
let models: [LanguageModel] = [
.openai(.gpt4o),
.openai(.gpt4oMini),
.openai(.gpt41),
.openai(.gpt4Turbo),
.openai(.custom("custom-openai")),
]
for model in models {
@ -65,15 +49,13 @@ enum ModelCapabilitiesTests {
#expect(capabilities.supportsMaxTokens)
#expect(capabilities.supportsFrequencyPenalty)
#expect(capabilities.supportsPresencePenalty)
#expect(capabilities.supportedProviderOptions.supportsParallelToolCalls)
#expect(capabilities.supportedProviderOptions.supportsResponseFormat)
#expect(capabilities.supportedProviderOptions.supportsLogprobs)
}
}
@Test
func `Claude models support thinking`() {
let models: [LanguageModel] = [
.anthropic(.opus47),
.anthropic(.opus4),
.anthropic(.sonnet4),
.anthropic(.sonnet45),
@ -151,7 +133,7 @@ enum ModelCapabilitiesTests {
struct SettingsValidationTests {
@Test
func `Validate settings for GPT-5.1`() {
func `Validate settings for GPT-5.5`() {
let settings = GenerationSettings(
maxTokens: 1000,
temperature: 0.7,
@ -166,7 +148,7 @@ enum ModelCapabilitiesTests {
),
)
let validated = settings.validated(for: .openai(.gpt51))
let validated = settings.validated(for: .openai(.gpt55))
#expect(validated.maxTokens == 1000)
#expect(validated.temperature == nil) // Excluded
@ -178,7 +160,7 @@ enum ModelCapabilitiesTests {
}
@Test
func `Validate settings for O3 with forced temperature`() {
func `Validate settings for GPT-5 strips unsupported options`() {
let settings = GenerationSettings(
temperature: 0.5,
topP: 0.8,
@ -190,16 +172,16 @@ enum ModelCapabilitiesTests {
),
)
let validated = settings.validated(for: LanguageModel.openai(.o4Mini))
let validated = settings.validated(for: LanguageModel.openai(.gpt55))
#expect(validated.temperature == 1.0) // Forced to 1.0
#expect(validated.temperature == nil) // Excluded
#expect(validated.topP == nil) // Excluded
#expect(validated.providerOptions.openai?.reasoningEffort == .high) // Kept
#expect(validated.providerOptions.openai?.verbosity == nil) // Removed
#expect(validated.providerOptions.openai?.reasoningEffort == nil) // Removed
#expect(validated.providerOptions.openai?.verbosity == .medium) // Kept
}
@Test
func `Validate settings for GPT-4`() {
func `Validate settings for custom OpenAI model`() {
let settings = GenerationSettings(
maxTokens: 2000,
temperature: 0.8,
@ -216,17 +198,13 @@ enum ModelCapabilitiesTests {
),
)
let validated = settings.validated(for: .openai(.gpt4o))
let validated = settings.validated(for: .openai(.custom("custom-openai")))
#expect(validated.maxTokens == 2000)
#expect(validated.temperature == 0.8)
#expect(validated.topP == 0.95)
#expect(validated.frequencyPenalty == 0.2)
#expect(validated.presencePenalty == 0.1)
#expect(validated.providerOptions.openai?.parallelToolCalls == true)
#expect(validated.providerOptions.openai?.responseFormat == .json)
#expect(validated.providerOptions.openai?.logprobs == true)
#expect(validated.providerOptions.openai?.topLogprobs == 3)
}
@Test
@ -308,7 +286,7 @@ enum ModelCapabilitiesTests {
func `Concurrent capability access`() async {
let models: [LanguageModel] = [
.openai(.gpt51),
.openai(.gpt4o),
.openai(.gpt55),
.anthropic(.opus4),
.google(.gemini25Flash),
]

View File

@ -9,9 +9,9 @@ struct ModelParsingTests {
}
@Test
func `parse GPT-5.1 base model`() {
let parsed = LanguageModel.parse(from: "gpt-5.1")
#expect(parsed == .openai(.gpt51))
func `parse GPT-5.5 base model`() {
let parsed = LanguageModel.parse(from: "gpt-5.5")
#expect(parsed == .openai(.gpt55))
}
@Test
@ -26,6 +26,12 @@ struct ModelParsingTests {
#expect(parsed == .openai(.gpt5Nano))
}
@Test
func `parse Claude Opus 4.7 model id`() {
let parsed = LanguageModel.parse(from: "claude-opus-4-7")
#expect(parsed == .anthropic(.opus47))
}
@Test
func `parse Claude Sonnet 4.5 snapshot id`() {
let parsed = LanguageModel.parse(from: "claude-sonnet-4-5-20250929")
@ -35,7 +41,7 @@ struct ModelParsingTests {
@Test
func `parse shorthand Claude alias`() {
let parsed = LanguageModel.parse(from: "claude")
#expect(parsed == .anthropic(.sonnet45))
#expect(parsed == .anthropic(.opus47))
}
@Test
@ -49,4 +55,24 @@ struct ModelParsingTests {
let parsed = LanguageModel.parse(from: "gemini")
#expect(parsed == .google(.gemini3Flash))
}
@Test
func `ModelSelector rejects legacy OpenAI before Ollama fallback`() throws {
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
for model in ["gpt-4o", "gpt-4.1", "gpt-3.5-turbo", "o4-mini", "o3-mini"] {
#expect(throws: ModelValidationError.self) {
_ = try ModelSelector.parseModel(model)
}
}
}
}
@Test
func `ModelSelector rejects Claude 3 before Ollama fallback`() throws {
if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
#expect(throws: ModelValidationError.self) {
_ = try ModelSelector.parseModel("claude-3-sonnet")
}
}
}
}

View File

@ -327,7 +327,7 @@ private struct MockTextProvider: ModelProvider {
private struct MockOpenAIProvider: ModelProvider {
var modelId: String {
"gpt-4"
"gpt-5.5"
}
var baseURL: String? {

View File

@ -89,12 +89,12 @@ struct StreamObjectTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestPerson.self,
)
#expect(result.model == .openai(.gpt4o))
#expect(result.model == .openai(.gpt55))
#expect(result.schema == TestPerson.self)
}
@ -157,7 +157,7 @@ struct StreamObjectTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestPerson.self,
)
@ -193,7 +193,7 @@ struct StreamObjectTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestPerson.self,
)
@ -221,7 +221,7 @@ struct StreamObjectTests {
let result = StreamObjectResult(
objectStream: testStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
schema: TestPerson.self,
)

View File

@ -231,7 +231,7 @@ struct StreamTransformTests {
let result = StreamTextResult(
stream: stream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -264,7 +264,7 @@ struct StreamTransformTests {
let result = StreamTextResult(
stream: stream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -291,7 +291,7 @@ struct StreamTransformTests {
let result = StreamTextResult(
stream: stream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)

View File

@ -367,35 +367,35 @@ struct TachikomaConfigurationTests {
Task {
// Uses default (.current)
_ = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [.user("Test")],
)
// Uses explicit
_ = try await generateText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [.user("Test")],
configuration: explicitConfig,
)
// Stream functions
_ = try await streamText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [.user("Test")],
)
_ = try await streamText(
model: .openai(.gpt4o),
model: .openai(.gpt55),
messages: [.user("Test")],
configuration: explicitConfig,
)
// Convenience functions
_ = try await generate("Test", using: .openai(.gpt4o))
_ = try await generate("Test", using: .openai(.gpt4o), configuration: explicitConfig)
_ = try await generate("Test", using: .openai(.gpt55))
_ = try await generate("Test", using: .openai(.gpt55), configuration: explicitConfig)
_ = try await stream("Test", using: .openai(.gpt4o))
_ = try await stream("Test", using: .openai(.gpt4o), configuration: explicitConfig)
_ = try await stream("Test", using: .openai(.gpt55))
_ = try await stream("Test", using: .openai(.gpt55), configuration: explicitConfig)
}
}
@ -559,9 +559,9 @@ struct TachikomaConfigurationTests {
return DummyProvider()
}
let provider = try config.makeProvider(for: .openai(.gpt4o))
let provider = try config.makeProvider(for: .openai(.gpt55))
#expect(provider is DummyProvider)
#expect(capturedModel == .openai(.gpt4o))
#expect(capturedModel == .openai(.gpt55))
}
@Test
@ -569,8 +569,8 @@ struct TachikomaConfigurationTests {
let config = TachikomaConfiguration(loadFromEnvironment: false)
config.setAPIKey("mock-key", for: .openai)
let provider = try config.makeProvider(for: .openai(.gpt4o))
#expect(provider is OpenAIProvider)
let provider = try config.makeProvider(for: .openai(.gpt55))
#expect(provider is OpenAIResponsesProvider)
}
}
}

View File

@ -317,7 +317,7 @@ struct ResponseCacheTests {
// Create a mock provider
let mockProvider = ResponseCacheMockProvider(
model: .openai(.gpt4o),
model: .openai(.gpt55),
response: ProviderResponse(text: "Cached response", usage: nil, finishReason: .stop),
)
@ -336,7 +336,7 @@ struct ResponseCacheTests {
// Use a simple counter that can be modified in the closure
let callCount = Box(value: 0)
var mockProvider = ResponseCacheMockProvider(
model: .openai(.gpt4o),
model: .openai(.gpt55),
response: ProviderResponse(text: "Response", usage: nil, finishReason: .stop),
)
mockProvider.onGenerateText = { _ in
@ -368,7 +368,7 @@ struct ResponseCacheTests {
let callCount = Box(value: 0)
var mockProvider = ResponseCacheMockProvider(
model: .openai(.gpt4o),
model: .openai(.gpt55),
response: ProviderResponse(text: "Test", usage: nil, finishReason: .stop),
)
mockProvider.onStreamText = { _ in

View File

@ -29,7 +29,7 @@ struct IntegrationTests {
let streamResult = StreamTextResult(
stream: textStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)

View File

@ -25,7 +25,7 @@ struct AnthropicInterleavedDefaultsTests {
@Test
func `Provider request includes beta header and thinking payload`() throws {
let config = TachikomaConfiguration(apiKeys: ["anthropic": "test-key"])
let provider = try AnthropicProvider(model: .opus45, configuration: config)
let provider = try AnthropicProvider(model: .opus47, configuration: config)
let settings = GenerationSettings(
maxTokens: 64,
@ -49,7 +49,7 @@ struct AnthropicInterleavedDefaultsTests {
let body = try #require(urlRequest.httpBody)
let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
#expect(json["model"] as? String == "claude-opus-4-5")
#expect(json["model"] as? String == "claude-opus-4-7")
#expect(json["stream"] as? Bool == true)
let thinking = try #require(json["thinking"] as? [String: Any])

View File

@ -20,7 +20,7 @@ private final class AzureTestURLProtocol: URLProtocol {
static let responseBody: Data = """
{
"id": "chatcmpl-azure",
"model": "gpt-4o",
"model": "gpt-5.5",
"choices": [
{
"index": 0,
@ -85,7 +85,7 @@ struct AzureOpenAIProviderTests {
await AzureTestURLProtocol.reset()
let provider = try AzureOpenAIProvider(
deploymentId: "gpt-4o",
deploymentId: "gpt-5.5",
resource: "my-aoai",
apiVersion: "2025-04-01-preview",
endpoint: nil,
@ -99,7 +99,7 @@ struct AzureOpenAIProviderTests {
#expect(response.text == "hello azure")
let sentRequest = await AzureTestURLProtocol.fetchLastRequest()
#expect(sentRequest?.url?.path == "/openai/deployments/gpt-4o/chat/completions")
#expect(sentRequest?.url?.path == "/openai/deployments/gpt-5.5/chat/completions")
if let components = sentRequest?.url.flatMap({ URLComponents(url: $0, resolvingAgainstBaseURL: false) }) {
let apiVersion = components.queryItems?.first { $0.name == "api-version" }?.value
@ -122,7 +122,7 @@ struct AzureOpenAIProviderTests {
await AzureTestURLProtocol.reset()
let provider = try AzureOpenAIProvider(
deploymentId: "gpt-4o-mini",
deploymentId: "gpt-5-mini",
resource: nil,
apiVersion: "2025-04-01-preview",
endpoint: nil,

View File

@ -49,7 +49,7 @@ struct ProviderIntegrationTests {
@Test(.enabled(if: Self.hasOpenAIKey))
func `OpenAI Provider - Real API Call`() async throws {
let model = Model.openai(.gpt4oMini)
let model = Model.openai(.gpt5Mini)
let config = TachikomaConfiguration()
do {
_ = try ProviderFactory.createProvider(for: model, configuration: config)
@ -72,7 +72,7 @@ struct ProviderIntegrationTests {
@Test(.enabled(if: Self.hasOpenAIKey))
func `OpenAI Provider - Tool Calling`() async throws {
let model = Model.openai(.gpt4oMini)
let model = Model.openai(.gpt5Mini)
let config = TachikomaConfiguration()
do {
@ -117,7 +117,7 @@ struct ProviderIntegrationTests {
@Test(.enabled(if: Self.hasOpenAIKey))
func `OpenAI Provider - Streaming`() async throws {
let model = Model.openai(.gpt4oMini)
let model = Model.openai(.gpt5Mini)
let config = TachikomaConfiguration()
do {
@ -323,7 +323,7 @@ struct ProviderIntegrationTests {
@Test(.enabled(if: Self.hasOpenAIKey))
func `Multi-Modal Provider - Vision Support`() async throws {
let model = Model.openai(.gpt4o)
let model = Model.openai(.gpt55)
let config = TachikomaConfiguration()
let provider = try ProviderFactory.createProvider(for: model, configuration: config)

View File

@ -9,10 +9,10 @@ struct ProviderSystemTests {
@Test
func `Provider Factory - OpenAI Provider Creation`() async throws {
try await TestHelpers.withTestConfiguration(apiKeys: ["openai": "test-key"]) { config in
let model = Model.openai(.gpt4o)
let model = Model.openai(.gpt55)
let provider = try ProviderFactory.createProvider(for: model, configuration: config)
#expect(provider.modelId == "gpt-4o")
#expect(provider.modelId == "gpt-5.5")
#expect(provider.capabilities.supportsVision == true)
#expect(provider.capabilities.supportsTools == true)
#expect(provider.capabilities.supportsStreaming == true)
@ -89,7 +89,7 @@ struct ProviderSystemTests {
}
#expect(throws: TachikomaError.self) {
try OpenAIProvider(model: .gpt4o, configuration: config)
try OpenAIProvider(model: .gpt55, configuration: config)
}
#expect(throws: TachikomaError.self) {
@ -102,9 +102,9 @@ struct ProviderSystemTests {
@Test
func `Model Capabilities - Vision Support`() {
#expect(Model.openai(.gpt4o).supportsVision == true)
#expect(Model.openai(.gpt4oMini).supportsVision == true)
#expect(Model.openai(.gpt41).supportsVision == false)
#expect(Model.openai(.gpt55).supportsVision == true)
#expect(Model.openai(.gpt5Mini).supportsVision == true)
#expect(Model.openai(.custom("text-only-openai")).supportsVision == false)
#expect(Model.anthropic(.opus4).supportsVision == true)
#expect(Model.anthropic(.sonnet4).supportsVision == true)
@ -121,8 +121,8 @@ struct ProviderSystemTests {
@Test
func `Model Capabilities - Tool Support`() {
#expect(Model.openai(.gpt4o).supportsTools == true)
#expect(Model.openai(.gpt41).supportsTools == true)
#expect(Model.openai(.gpt55).supportsTools == true)
#expect(Model.openai(.gpt55).supportsTools == true)
#expect(Model.anthropic(.opus4).supportsTools == true)
#expect(Model.anthropic(.sonnet4).supportsTools == true)
@ -136,7 +136,7 @@ struct ProviderSystemTests {
@Test
func `Model Capabilities - Streaming Support`() {
#expect(Model.openai(.gpt4o).supportsStreaming == true)
#expect(Model.openai(.gpt55).supportsStreaming == true)
#expect(Model.anthropic(.opus4).supportsStreaming == true)
#expect(Model.grok(.grok4).supportsStreaming == true)
#expect(Model.ollama(.llama33).supportsStreaming == true)

View File

@ -23,6 +23,7 @@ struct OpenAIResponsesProviderTests {
let config = self.openAIConfig()
let gpt5Models: [LanguageModel.OpenAI] = [
.gpt55,
.gpt52,
.gpt51,
.gpt5,
@ -44,8 +45,8 @@ struct OpenAIResponsesProviderTests {
}
@Test
func `GPT-5.1 text.verbosity parameter is set correctly`() throws {
// Test that the text.verbosity parameter is properly configured for GPT-5.1
func `GPT-5.5 text.verbosity parameter is set correctly`() throws {
// Test that the text.verbosity parameter is properly configured for GPT-5.5
let config = self.openAIConfig()
// Skip if no API key
@ -54,7 +55,7 @@ struct OpenAIResponsesProviderTests {
}
let provider = try OpenAIResponsesProvider(
model: .gpt51,
model: .gpt55,
configuration: config,
)
@ -69,18 +70,18 @@ struct OpenAIResponsesProviderTests {
// We can't directly test the internal request building without making it public
// But we can verify the provider is configured correctly
#expect(provider.modelId == "gpt-5.1")
#expect(provider.modelId == "gpt-5.5")
#expect(provider.capabilities.supportsTools == true)
#expect(provider.capabilities.supportsVision == true)
}
@Test
func `Reasoning models use Responses API`() throws {
// Test that reasoning-oriented models also use the OpenAIResponsesProvider
func `GPT-5 models use Responses API`() throws {
// Test that GPT-5 models use the OpenAIResponsesProvider
let config = self.openAIConfig()
let reasoningModels: [LanguageModel.OpenAI] = [
.o4Mini,
let responsesModels: [LanguageModel.OpenAI] = [
.gpt55,
.gpt52,
.gpt51,
.gpt5,
@ -88,7 +89,7 @@ struct OpenAIResponsesProviderTests {
.gpt5Thinking,
]
for model in reasoningModels {
for model in responsesModels {
let provider = try ProviderFactory.createProvider(
for: .openai(model),
configuration: config,
@ -102,11 +103,10 @@ struct OpenAIResponsesProviderTests {
}
@Test
func `Legacy models use standard OpenAI provider`() throws {
// Test that non-GPT-5/reasoning models use the standard OpenAIProvider
func `Custom OpenAI models use standard OpenAI provider`() throws {
let config = self.openAIConfig()
let legacyModels: [LanguageModel.OpenAI] = [.gpt4o, .gpt4oMini, .gpt41]
let legacyModels: [LanguageModel.OpenAI] = [.custom("custom-openai")]
for model in legacyModels {
let provider = try ProviderFactory.createProvider(
@ -116,7 +116,7 @@ struct OpenAIResponsesProviderTests {
#expect(
provider is OpenAIProvider,
"Legacy model \(model) should use OpenAIProvider",
"Custom model \(model) should use OpenAIProvider",
)
}
}
@ -472,12 +472,12 @@ struct OpenAIResponsesProviderTests {
#expect(request.url?.path == "/v1/responses")
let payload = Self.responsesStreamPayload(chunks: [
Self.streamChunkJSON(content: "Hello", finishReason: nil),
Self.streamChunkJSON(content: "Hello world", finishReason: nil),
Self.streamChunkJSON(content: " world", finishReason: nil),
Self.streamChunkJSON(content: nil, finishReason: "stop"),
])
return NetworkMocking.streamResponse(for: request, data: payload)
} operation: { session in
let provider = try OpenAIResponsesProvider(model: .o4Mini, configuration: config, session: session)
let provider = try OpenAIResponsesProvider(model: .gpt55, configuration: config, session: session)
let stream = try await provider.streamText(request: self.sampleRequest)
var collected = ""
@ -566,29 +566,18 @@ struct OpenAIResponsesProviderTests {
}
private static func streamChunkJSON(content: String?, finishReason: String?) -> String {
var delta: [String: Any] = [
"role": "assistant",
]
if let content {
delta["content"] = content
}
var choice: [String: Any] = [
"index": 0,
"delta": delta,
]
if let finishReason {
choice["finish_reason"] = finishReason
let chunk: [String: Any] = [
"type": "response.output_text.delta",
"delta": content,
]
let data = try! JSONSerialization.data(withJSONObject: chunk)
return String(data: data, encoding: .utf8)!
}
let chunk: [String: Any] = [
"id": "resp_stream",
"object": "response",
"created": 1_700_000_000,
"model": "o4-mini",
"choices": [choice],
"type": finishReason == nil ? "response.output_text.done" : "response.completed",
]
let data = try! JSONSerialization.data(withJSONObject: chunk)
return String(data: data, encoding: .utf8)!
}

View File

@ -57,7 +57,7 @@ struct ProviderEndToEndTests {
let config = Self.makeConfiguration { config in
config.setAPIKey("sk-live-openai", for: .openai)
}
let provider = try OpenAIProvider(model: .gpt4o, configuration: config)
let provider = try OpenAIProvider(model: .gpt55, configuration: config)
let response = try await provider.generateText(request: Self.basicRequest)
#expect(response.text == "OpenAI chat success")
}
@ -410,7 +410,7 @@ struct ProviderEndToEndTests {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1_723_000_000,
"model": "gpt-4o",
"model": "gpt-5.5",
"choices": [
[
"index": 0,

View File

@ -102,7 +102,7 @@ struct UIIntegrationTests {
let streamResult = StreamTextResult(
stream: textStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)
@ -159,7 +159,7 @@ struct UIIntegrationTests {
let streamResult = StreamTextResult(
stream: textStream,
model: .openai(.gpt4o),
model: .openai(.gpt55),
settings: .default,
)

View File

@ -60,7 +60,7 @@ struct UnifiedErrorsTests {
statusCode: 429,
responseBody: "{\"error\": \"rate_limit\"}",
provider: "openai",
modelId: "gpt-4",
modelId: "gpt-5.5",
requestId: "req-123",
retryAfter: 30,
metadata: ["tokens_used": "5000"],
@ -120,7 +120,7 @@ struct UnifiedErrorsTests {
statusCode: 500,
responseBody: "Internal server error",
provider: "anthropic",
modelId: "claude-3",
modelId: "claude-opus-4-7",
requestId: "req-456",
errorType: .serverError,
message: "Server encountered an error",

View File

@ -48,7 +48,7 @@ struct UsageTrackingTests {
let tracker = UsageTracker(forTesting: true)
let sessionId = tracker.startSession()
let model = LanguageModel.openai(.gpt4oMini)
let model = LanguageModel.openai(.gpt5Mini)
let usage = Usage(inputTokens: 100, outputTokens: 50)
// Record usage
@ -65,7 +65,7 @@ struct UsageTrackingTests {
#expect(session?.totalTokens == 150)
let operation = session?.operations.first
#expect(operation?.modelId == "gpt-4o-mini")
#expect(operation?.modelId == "gpt-5-mini")
#expect(operation?.providerName == "OpenAI")
#expect(operation?.usage.inputTokens == 100)
#expect(operation?.usage.outputTokens == 50)
@ -83,7 +83,7 @@ struct UsageTrackingTests {
let tracker = UsageTracker(forTesting: true)
let sessionId = tracker.startSession()
let model = LanguageModel.openai(.gpt4oMini)
let model = LanguageModel.openai(.gpt5Mini)
// Record multiple operations
tracker.recordUsage(
@ -117,10 +117,10 @@ struct UsageTrackingTests {
let usage = Usage(inputTokens: 1_000_000, outputTokens: 1_000_000) // 1M tokens each for easy calculation
// Test OpenAI pricing
let gpt4oMiniCost = calculator.calculateCost(for: .openai(.gpt4oMini), usage: usage)
#expect(gpt4oMiniCost.input == 0.15) // $0.15 per million input tokens
#expect(gpt4oMiniCost.output == 0.60) // $0.60 per million output tokens
#expect(gpt4oMiniCost.total == 0.75)
let gpt5MiniCost = calculator.calculateCost(for: .openai(.gpt5Mini), usage: usage)
#expect(gpt5MiniCost.input == 1.00)
#expect(gpt5MiniCost.output == 4.00)
#expect(gpt5MiniCost.total == 5.00)
// Test Anthropic pricing
let claudeHaikuCost = calculator.calculateCost(for: .anthropic(.haiku45), usage: usage)
@ -145,7 +145,7 @@ struct UsageTrackingTests {
let session1 = tracker.startSession()
tracker.recordUsage(
sessionId: session1,
model: .openai(.gpt4oMini),
model: .openai(.gpt5Mini),
usage: Usage(inputTokens: 100, outputTokens: 50),
operation: .textGeneration,
)
@ -173,7 +173,7 @@ struct UsageTrackingTests {
// Check model breakdown
#expect(totalUsage.modelBreakdown.count == 2)
#expect(totalUsage.modelBreakdown[LanguageModel.openai(.gpt4oMini).modelId] != nil)
#expect(totalUsage.modelBreakdown[LanguageModel.openai(.gpt5Mini).modelId] != nil)
#expect(totalUsage.modelBreakdown[LanguageModel.anthropic(.haiku45).modelId] != nil)
// Check operation breakdown
@ -191,7 +191,7 @@ struct UsageTrackingTests {
let sessionId = tracker.startSession()
tracker.recordUsage(
sessionId: sessionId,
model: .openai(.gpt4oMini),
model: .openai(.gpt5Mini),
usage: Usage(inputTokens: 1000, outputTokens: 500),
operation: .textGeneration,
)
@ -212,7 +212,7 @@ struct UsageTrackingTests {
#expect(formattedReport.contains("Operations: 1"))
#expect(formattedReport.contains("Total Tokens: 1500"))
#expect(formattedReport.contains("OpenAI"))
#expect(formattedReport.contains("gpt-4o-mini"))
#expect(formattedReport.contains("gpt-5-mini"))
#expect(formattedReport.contains("Text Generation"))
}
@ -223,7 +223,7 @@ struct UsageTrackingTests {
let sessionId = tracker.startSession()
tracker.recordUsage(
sessionId: sessionId,
model: .openai(.gpt4oMini),
model: .openai(.gpt5Mini),
usage: Usage(inputTokens: 100, outputTokens: 50),
operation: .textGeneration,
)

View File

@ -8,14 +8,11 @@ Tachikoma ships with a built-in model catalog (`CaseIterable` enums) plus suppor
## OpenAI (`LanguageModel.OpenAI`)
- `o4-mini`
- `gpt-5.5`
- `gpt-5.2`, `gpt-5.1`
- `gpt-5`, `gpt-5-pro`, `gpt-5-mini`, `gpt-5-nano`
- `gpt-5-thinking`, `gpt-5-thinking-mini`, `gpt-5-thinking-nano`
- `gpt-5-chat-latest`
- `gpt-4.1`, `gpt-4.1-mini`
- `gpt-4o`, `gpt-4o-mini`, `gpt-4o-realtime-preview`
- `gpt-4-turbo`, `gpt-3.5-turbo`
Notes:
- Mini/Nano variants exist only for **GPT5** (not for GPT5.1 / GPT5.2).

View File

@ -9,7 +9,7 @@ Based on the Vercel AI SDK patterns from `/Users/steipete/Downloads/ai-sdk.md`,
The Vercel AI SDK provides these core patterns:
- **Provider-specific imports**: `import { openai } from '@ai-sdk/openai'`
- **Core functions**: `generateText()`, `streamText()`, `generateObject()`, `streamObject()`
- **Model specification**: `model: openai('gpt-4o')`, `model: anthropic('claude-sonnet-4')`
- **Model specification**: `model: openai('gpt-5.5')`, `model: anthropic('claude-sonnet-4')`
- **Unified API**: Same function signatures across all providers
- **Tool integration**: Simple `tools: { toolName: toolDefinition }` object
- **Streaming first**: Built-in streaming support with AsyncSequence patterns
@ -55,7 +55,7 @@ The Vercel AI SDK provides these core patterns:
- [ ] **Replace LanguageModel enum with LanguageModel protocol**
- [ ] Create `LanguageModel` protocol with provider metadata
- [ ] Create provider-specific model structs: `OpenAIModel`, `AnthropicModel`, etc.
- [ ] Implement model creation: `openai("gpt-4o")`, `anthropic("claude-sonnet-4")`
- [ ] Implement model creation: `openai("gpt-5.5")`, `anthropic("claude-sonnet-4")`
- [ ] Add model capabilities: `supportsTools`, `supportsStreaming`, `supportsImages`
#### 1.3 Core API Functions Redesign
@ -105,7 +105,7 @@ The Vercel AI SDK provides these core patterns:
- [ ] Google Gemini API integration
- [ ] **Provider-specific features**
- [ ] Reasoning support for GPT-5 and o4-mini models
- [ ] Reasoning support for GPT-5 thinking models
- [ ] Vision support for multimodal models
- [ ] Function calling for compatible models
- [ ] Streaming optimization per provider