[codex] Add FoundationModels facade and provider expansion#65
Conversation
Adds ConduitLanguageModel/ConduitLanguageModelSession compatibility APIs, delegate-aware tool execution, typed provider custom options, Gemini/Open Responses/Ollama provider surfaces, and focused tests/docs.\n\nVerification already run: swift build; swift test; targeted facade, tool delegate, custom options, Gemini, Open Responses, and Ollama tests; trait builds for OpenAI, Gemini, and Ollama.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f2d5b0a00
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let finishReason = toolCalls.isEmpty ? FinishReason.stop : .toolCalls | ||
| return GenerationResult( | ||
| text: toolCalls.isEmpty ? text : "", |
There was a problem hiding this comment.
Preserve Gemini candidate finish reason in parsed result
The Gemini response parser ignores candidates[0].finishReason and always returns .stop when there are no tool calls. Gemini uses finishReason to signal truncation/safety/block outcomes, so this mapping can report incomplete or filtered generations as successful stops and break caller logic that retries/handles non-stop endings.
Useful? React with 👍 / 👎.
| private nonisolated func generationResult(text: String, json: [String: Any]) -> GenerationResult { | ||
| let completionTokens = json["eval_count"] as? Int ?? 0 | ||
| let promptTokens = json["prompt_eval_count"] as? Int ?? 0 | ||
| let finishReason: FinishReason = (json["done"] as? Bool) == false ? .maxTokens : .stop |
There was a problem hiding this comment.
Do not map in-progress Ollama chunks to max-tokens finish
done == false in Ollama stream payloads means the generation is still in progress, not that it stopped because of token limits. Mapping every non-final chunk to .maxTokens emits incorrect termination semantics to stream consumers (and final chunks are always .stop regardless of provider done_reason), so downstream code can misinterpret successful streams as token-limit stops.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d43a0c147
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| guard let payload = data.data(using: .utf8), | ||
| let result = try? parseGenerationResponse(data: payload) else { | ||
| return nil |
There was a problem hiding this comment.
Surface Gemini stream parse/API errors
decodeStreamEvent turns any parsing or API error into nil with try?, and the streaming loop skips nil events. In streaming mode this can silently drop Gemini error payloads (or malformed chunks) and finish without throwing, so callers may treat a failed/incomplete generation as success with partial output.
Useful? React with 👍 / 👎.
| var body: [String: Any] = [ | ||
| "model": model.rawValue, | ||
| "messages": messages.map { message in | ||
| var serialized: [String: Any] = [ | ||
| "role": message.role.rawValue, | ||
| "content": message.content.textValue |
There was a problem hiding this comment.
Include configured tools in Ollama chat requests
This request body construction never serializes config.tools, so tool definitions are not sent to /api/chat. For Ollama tool-calling flows, that prevents the model from receiving callable tool schemas, which means sessions that register tools in Conduit cannot reliably enter the tool-call loop on this provider.
Useful? React with 👍 / 👎.
| let promptEntry = makePromptEntry(prompt, options: options, responseFormat: nil) | ||
| append(.prompt(promptEntry)) | ||
|
|
||
| let text = try await session.run(promptEntry.textContent, config: promptEntry.generateConfig) |
There was a problem hiding this comment.
Preserve session tool config when responding with options
ConduitLanguageModelSession.respond passes promptEntry.generateConfig as a full per-call config, but that config only carries prompt options/response format and drops session-level tool definitions. Because ChatSession.send(..., config:) uses the override directly, initializing ConduitLanguageModelSession with tools: can result in requests that no longer advertise tools to providers that require schemas each turn.
Useful? React with 👍 / 👎.
Summary
ConduitLanguageModelandConduitLanguageModelSessionas a FoundationModels-familiar facade over the existing Conduit provider/session runtime.GenerateConfig.Verification
swift buildswift testswift test --filter ConduitLanguageModelSessionTestsswift test --filter ToolExecutionDelegateTestsswift test --filter GenerateConfigCustomOptionsTestsswift test --traits Gemini --filter GeminiProviderTestsswift test --traits OpenAI --filter OpenResponsesProviderTestsswift test --traits Ollama --filter OllamaProviderTestsswift build --traits OpenAIswift build --traits Geminiswift build --traits OllamaNotes
.build-default/,Examples/, andRESEARCH_DynamicGenerationSchema.mdwere preserved and not included.