feat: add GPT-5.5 and Claude Opus 4.7 models
This commit is contained in:
parent
3765b08186
commit
65d386c19e
10
CHANGELOG.md
10
CHANGELOG.md
@ -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 GPT‑5 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 GPT‑5 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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
```
|
||||
|
||||
|
||||
@ -163,8 +163,7 @@ final class Agent {
|
||||
switch model {
|
||||
case let .openai(openaiModel):
|
||||
switch openaiModel {
|
||||
case .o4Mini,
|
||||
.gpt5,
|
||||
case .gpt5,
|
||||
.gpt5Pro,
|
||||
.gpt5Mini,
|
||||
.gpt5Nano,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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!"
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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],
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
]
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,7 +327,7 @@ private struct MockTextProvider: ModelProvider {
|
||||
|
||||
private struct MockOpenAIProvider: ModelProvider {
|
||||
var modelId: String {
|
||||
"gpt-4"
|
||||
"gpt-5.5"
|
||||
}
|
||||
|
||||
var baseURL: String? {
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -29,7 +29,7 @@ struct IntegrationTests {
|
||||
|
||||
let streamResult = StreamTextResult(
|
||||
stream: textStream,
|
||||
model: .openai(.gpt4o),
|
||||
model: .openai(.gpt55),
|
||||
settings: .default,
|
||||
)
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)!
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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 **GPT‑5** (not for GPT‑5.1 / GPT‑5.2).
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user