Skip to content

feat(providers): add native OpenAI provider on Pydantic AI - #421

Merged
Jason Robert (jrob5756) merged 15 commits into
microsoft:mainfrom
hertznsk:feat/openai-pydantic-provider
Aug 22, 2026
Merged

feat(providers): add native OpenAI provider on Pydantic AI#421
Jason Robert (jrob5756) merged 15 commits into
microsoft:mainfrom
hertznsk:feat/openai-pydantic-provider

Conversation

@hertznsk

Copy link
Copy Markdown
Contributor

Summary

This PR adds a native OpenAI-compatible provider backed by the shared Pydantic AI runtime introduced for Claude in #355. It also extracts the common Pydantic AI execution pipeline so the Claude and OpenAI adapters share Conductor-owned retry, interrupt, event, MCP, structured-output, usage, and partial-output behavior.

Together with the already merged #355, this closes#315 by demonstrating and shipping Pydantic AI as the shared inner runtime for direct Anthropic and OpenAI providers.

What changed

  • adds an openai provider using Pydantic AI and the OpenAI Chat Completions API;
  • supports OpenAI-compatible endpoints through YAML and OPENAI_API_KEY / OPENAI_BASE_URL;
  • preserves Conductor provider contracts for events, retries, interrupts, MCP tools, structured output, usage accounting, reasoning effort, and working directories;
  • extracts the shared Pydantic AI execution pipeline from the Claude provider;
  • adds provider registration, capability validation, diagnostics, documentation, an example workflow, and focused HTTP-stub/integration coverage;
  • completes Claude dialog-agent limits while keeping the shared runtime behavior aligned.

Practical validation

Builds from this branch have been used for an extended period on real working workflows. No shortcomings were identified during that use beyond the separately reported MCP 2.0 compatibility issue in #419. The issue is not specific to this provider implementation and is intentionally tracked separately.

Verification

  • rebased onto the latest origin/main;
  • verified conflict-free against origin/main;
  • make check;
  • make test: 6295 passed, 39 skipped, 8 deselected;
  • CLI smoke test: conductor --help;
  • validation smoke tests for examples/openai-compatible.yaml and a missing workflow path.

Closes#315.

Related: #355, #419.

@jrob5756Jason Robert (jrob5756) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for another great contribution!

The shared-runner extraction itself checks out. I diffed the pre-PR Claude execute() against claude.py plus runner.py line by line and the behaviour is preserved, the factory drops nothing, and event parity, last_call_input_tokens, cache-inclusive tokens and structured-output recovery are all inherited rather than reimplemented. That is the payoff #315 predicted, and it is the right shape.

Four things stand out as blocking:

  1. Ambient OPENAI_API_KEY reaches a custom base_url, and the guard meant to stop it is unreachable.
  2. reasoning.effort is never checked against the model, which an explicit parity rule requires.
  3. agent_reasoning_events=True cannot be honoured on Chat Completions.
  4. runtime.temperature above 1.0 is no longer rejected on the run path.

Two more that are not inline anywhere:

AGENTS.md lost about a thousand characters of unrelated content. The Run/Resume Parity note now stops at "hard-stops a healthy workflow." Both the merge base and origin/main still carry the tail covering the dialog_message rationale, the replayMode latching in Header.tsx and use-replay.ts, and the EventLogSubscriber append-mode note. It disappears inside this branch's own history, so it reads as a rebase artifact, but merging as-is silently reverts documentation for behaviour this PR does not touch.

tier="stable" has no real-API test. The repo's own promotion criteria ask for one, and there is a real_api marker plus a Claude precedent to mirror. Shipping as experimental first would also be a fair option, given the descriptor issues above.

On naming: openai is the right choice and matches claude in naming the model API rather than the vendor. The thing to fix is openai-agents sitting next to it. See the comment on config/schema.py.

Comment threadsrc/conductor/providers/openai.py Outdated
Comment on lines +244 to +252
effective_api_key = self._api_key
if effective_api_key is None:
effective_api_key = os.environ.get("OPENAI_API_KEY")

if not effective_api_key:
raise ValidationError(
"OPENAI_API_KEY environment variable is not set and no api_key was provided",
suggestion="Set OPENAI_API_KEY or pass api_key to the provider.",
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_initialize_client reads the ambient OPENAI_API_KEY even when a custom base_url is configured, so a user's real OpenAI key is sent to whatever endpoint the workflow points at. Exporting OPENAI_BASE_URL alone is enough to trigger it, with no YAML opt-in.

Two knock-on effects. The docstring at line 170 promises the opposite behaviour. And because line 254 writes the resolved key back to self._api_key, the guard in agent_builder.py:110-114 can never fire — the test covering it calls build_agent directly, so it passes while the shipped provider does the reverse.

copilot.py refuses this fallback on purpose, and docs/configuration.md:164 explains why.

Suggested change
effective_api_key=self._api_key
ifeffective_api_keyisNone:
effective_api_key=os.environ.get("OPENAI_API_KEY")
ifnoteffective_api_key:
raiseValidationError(
"OPENAI_API_KEY environment variable is not set and no api_key was provided",
suggestion="Set OPENAI_API_KEY or pass api_key to the provider.",
)
effective_api_key=self._api_key
ifeffective_api_keyisNoneandself._base_urlisNone:
effective_api_key=os.environ.get("OPENAI_API_KEY")
ifnoteffective_api_key:
ifself._base_urlisnotNone:
raiseValidationError(
"A custom base_url requires an explicit api_key",
suggestion=(
"Set runtime.provider.api_key in YAML. Conductor will not "
"forward an ambient OPENAI_API_KEY to a non-OpenAI endpoint."
),
)
raiseValidationError(
"OPENAI_API_KEY environment variable is not set and no api_key was provided",
suggestion="Set OPENAI_API_KEY or pass api_key to the provider.",
)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. _initialize_client no longer falls back to the ambient OPENAI_API_KEY when a custom base_url is set — that combination now raises ValidationError at construction, and the resolved key is never written back to self._api_key, so the agent_builder.py guard is reachable in production, not just in its direct unit test. When no custom base_url is configured the ambient key is still honored (the zero-YAML default-endpoint path). The __init__ docstring now states the rule instead of contradicting it. Covered by new tests in test_openai.py (custom base_url + ambient key raises; explicit key + base_url works; ambient-only keeps _api_key is None).

Comment threadsrc/conductor/providers/openai.py Outdated
streaming_events=True,
# Reasoning content is surfaced as ``agent_reasoning`` events when the model
# returns it.
agent_reasoning_events=True,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAIChatModel speaks Chat Completions, and pydantic-ai only builds a ThinkingPart from reasoning / reasoning_content, which its own source notes is a DeepSeek and Moonshot field. OpenAI returns reasoning summaries through the Responses API only, so against api.openai.com this event never fires. The claim holds only for third-party proxies that echo reasoning_content.

capabilities.py:67 asks for the weaker value when a claim cannot be honoured under all conditions, and tier="stable" makes that stricter rather than looser.

Suggested change
agent_reasoning_events=True,
# Chat Completions never returns reasoning content. This becomes True
# only on an OpenAIResponsesModel backend.
agent_reasoning_events=False,

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — agent_reasoning_events=False with a comment explaining that Chat Completions never returns reasoning content from api.openai.com (only third-party proxies echoing reasoning_content would surface it), and that this flips to True only on an OpenAIResponsesModel backend. Test updated to assert the weaker value.

Comment threadsrc/conductor/providers/openai.py Outdated
# returns it.
agent_reasoning_events=True,
# OpenAI supports reasoning_effort on its reasoning models (o-series, etc.).
reasoning_effort=("low", "medium", "high", "xhigh"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

o1, o1-mini, o3, o3-mini and o4-mini accept low, medium and high only. xhigh arrived with the GPT-5.1-Codex-Max generation, so the two models named in docs/providers/openai.md:206 and AGENTS.md:370 are precisely the ones that will 400 on it.

hermes.py handles the same uncertainty well: it declares the narrower tuple and spends a comment explaining that upstream support is unverified.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the tuple is now ("low", "medium", "high") with a hermes-style comment explaining that xhigh arrived with the GPT-5.1-Codex-Max generation and that o1/o3-mini/o4-mini accept only low/medium/high. docs/providers/openai.md's effort matrix now marks xhigh as rejected, and the provider-level tuple tests assert the narrower set.

if timeout is not None:
settings["timeout"] = timeout
if effort is not None:
settings["openai_reasoning_effort"] = effort

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md requires every provider to validate the requested effort against the selected model and raise ValidationError when the model does not support it. execute() only checks membership in CAPABILITIES.reasoning_effort, which is a provider-level claim, so openai_reasoning_effort gets set for any model at all.

With gpt-4o this reaches the API, returns a 400, is classified non-retryable in retry.py:133, and lands on the user mid-run as Pydantic AI provider error: ... rather than a config error caught before anything ran. _resolve_anthropic_thinking a few hundred lines up is the shape to copy. Sharing the model check with get_model_capabilities would stop the two drifting.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — _build_openai_model_settings now validates the resolved effort against the model using pydantic_ai.profiles.openai.openai_model_profile(...).openai_supports_reasoning via a shared _openai_model_supports_reasoning helper (returns None on older pydantic-ai so unknown means skip, never guess). A non-reasoning model (e.g. gpt-4o) with an effort configured raises ValidationError at agent-build time, mirroring _resolve_anthropic_thinking. The same helper is reused by get_model_capabilities and execute_dialog_turn. Tests cover gpt-4o rejection, o3-mini acceptance, and the default-model resolution path.

Comment threadsrc/conductor/providers/openai.py Outdated

if max_tokens is not None:
self._validate_max_tokens(max_tokens)
self._default_max_tokens = max_tokens or 4096

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pydantic-ai maps max_tokens onto max_completion_tokens, which on reasoning models counts reasoning tokens as well as output. The default model here is gpt-5-mini, so an unconfigured workflow can spend the whole 4096 on reasoning and come back with empty content. That surfaces as UnexpectedModelBehavior, which the retry layer treats as retryable, so it exhausts the budget and finishes on the misleading "Model repeatedly failed to produce valid structured output".

Anthropic requires an output cap. OpenAI does not, so there is no need to invent one.

Suggested change
self._default_max_tokens=max_tokensor4096
self._default_max_tokens=max_tokens

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — self._default_max_tokens = max_tokens (no invented 4096 cap). The docstring now states that None leaves the parameter unset so the server applies its own default. The dialog path keeps its explicit 4096 cap, which is intentional: dialog turns are short text replies.

Comment threadsrc/conductor/providers/factory.py Outdated
ProviderType = Literal[
"copilot",
"openai",
"openai-agents",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same removal as config/schema.py:2547. While the unions are being touched, they have drifted from each other: AgentDef.provider and ProviderSettings.name differ by {aca, openai-agents}, and this PR widens registry.ProviderType further. One shared alias would keep them honest.

If the member has to stay for now, the branch at line 143 should stop recommending copilot, since the accurate answer is the provider this PR adds.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — all four provider-name Literals now share one alias, ProviderName, defined in config/schema.py and imported by factory.py and registry.py (schema imports nothing from providers, so no cycle). AgentDef.provider and ProviderSettings.name use the same alias, which also fixes the drift you noted (AgentDef.provider was missing aca). The factory's case _ suggestion no longer recommends copilot for a removed name; it lists the actual set. Tests asserting the old not yet implemented behavior now assert the schema rejection.

Comment threadpyproject.toml Outdated
"anthropic>=0.77.0,<1.0.0",
"pydantic-ai>=1.44.0",
"pydantic-ai-slim[anthropic,openai]>=1.44.0",
"openai>=2.48.0,<2.49.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A one-minor window is far tighter than every sibling pin here (anthropic>=0.77.0,<1.0.0, mcp>=1.28.1), and it will block patch releases and conflict with anything else in the environment wanting a newer openai. Nothing in the code requires <2.49; 2.48 is only the floor for xhigh in ReasoningEffort.

The line above is a package swap rather than an added extra: pydantic-ai to pydantic-ai-slim drops pydantic_ai.mcp. That is harmless because Conductor ships its own toolset, but it belongs in the CHANGELOG.

Suggested change
"openai>=2.48.0,<2.49.0",
"openai>=2.48.0,<3.0.0",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — openai>=2.48.0,<3.0.0, and uv.lock regenerated. The pydantic-aipydantic-ai-slim[anthropic,openai] swap is now documented in the CHANGELOG entry.

"authenticates via `claude login`; ANTHROPIC_API_KEY is an optional override"
),
),
"openai": _CredentialSpec(env_vars=("OPENAI_API_KEY", "OPENAI_BASE_URL")),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With optional_auth_note left at None, the dataclass docstring says the provider genuinely requires one of these env vars, so cli/doctor.py:262 renders an unset OPENAI_BASE_URL as a red cross for every user on the standard endpoint. The claude entry above lists only real credentials and deliberately leaves ANTHROPIC_BASE_URL out.

Suggested change
"openai": _CredentialSpec(env_vars=("OPENAI_API_KEY","OPENAI_BASE_URL")),
"openai": _CredentialSpec(env_vars=("OPENAI_API_KEY",)),

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — "openai": _CredentialSpec(env_vars=("OPENAI_API_KEY",)). An unset OPENAI_BASE_URL no longer renders as a red cross for standard-endpoint users. The diagnostics test asserting the old pair is updated.

if isinstance(exception, ProviderError):
return exception.is_retryable

if ModelHTTPError is not None and isinstance(exception, ModelHTTPError):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pydantic-ai wraps every Anthropic APIStatusError at 400 or above into ModelHTTPError, so this branch is on Claude's hot path too. Against the merge base, 429 and 5xx flip from fatal to retryable. That is a genuine fix, but it changes an existing provider's behaviour with no CHANGELOG entry and no note in the parity section.

The deny-list shape also makes 409, 413, 422 and 451 retryable, and none of those recover, so they now burn the full budget with backoff. An allow-list of 5xx plus 429 and 408 says what you mean.

Worth pairing with _get_retry_after, which returns None for ModelHTTPError, so a 429 Retry-After header is ignored on both providers.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly addressed by the rebase — while this PR was in flight, origin/main landed #458 which rewrote _is_retryable_error to the allow-list you describe (429 or 500 <= code < 600 for ModelHTTPError). Our branch's deny-list hunk was dropped in the conflict resolution. On top of that we added 408 to the allow-list and an explicit openai.APIStatusError isinstance arm, since the OpenAI SDK's errors reach the shared runner untranslated on this path. CHANGELOG now notes the retry-classification change. Fair point on _get_retry_after returning None for ModelHTTPError — left as-is since that's pre-existing on main and deserves its own issue.

Comment threaddocs/providers/openai.md Outdated

```bash
# Using uv (recommended)
uv add 'openai>=1.0.0'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openai is a core dependency pinned at >=2.48.0,<2.49.0 in pyproject.toml, so this asks for a version two majors below the project's own floor and installs something the reader already has.

The matching troubleshooting entry at line 245 describes a state that cannot occur either: agent_builder.py imports openai and pydantic_ai.models.openai at module scope, so a missing SDK raises ImportError well before the friendly ProviderError can run. Contrast claude-agent-sdk and aca, which are real extras where that guard earns its place.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the "Install the OpenAI SDK" quick-start step and the matching troubleshooting entry are deleted; openai is a core dependency pinned at >=2.48.0, and the module-scope import in agent_builder.py means a missing SDK raises ImportError long before the friendly ProviderError. Quick Start is renumbered.

hertznskand others added 13 commits August 19, 2026 21:54
Drive the real OpenAIChatModel + AsyncOpenAI through run_agent_pipeline
with httpx.MockTransport. Covers success usage mapping, 400 non-retryable
(one request), and 429 retryable then success (two requests).
…gration
Add todo-11 test coverage for the openai provider:
- factory: string + structured construction, YAML api_key precedence over env,
missing API key raises ValidationError naming OPENAI_API_KEY
- validate-level: runtime/provider: openai, per-agent provider: openai,
reasoning.effort: max static rejection, tools allowlist, mcp_servers, skills
- retry: existing openai.RateLimitError/BadRequestError tests already cover the
requirement; no new code needed
- SecretStr redaction for openai ProviderSettings
- temperature: schema-level 1.5 valid / 2.5 invalid, validator-level mixed
default=openai + per-agent/for_each inline claude override fails naming
temperature and claude
Also exclude openai-named example files from the copilot backward-compatibility
helper so the existing copilot-only assertion does not trip over
examples/openai-compatible.yaml.
Verification: uv run pytest tests/test_config tests/test_providers/test_factory.py tests/test_providers/test_pydantic_ai_retry.py -x (1253 passed, 9 skipped); make check clean.
Address every review comment on microsoft#421:
- openai.py: never forward an ambient OPENAI_API_KEY to a custom
base_url; the env-resolved key no longer overwrites self._api_key, so
the agent_builder guard against custom endpoints without an explicit
key is reachable in production, not just in direct unit tests.
- openai.py: declare agent_reasoning_events=False — Chat Completions
never returns reasoning content from api.openai.com; only
DeepSeek/Moonshot-style proxies echo reasoning_content.
- openai.py: narrow reasoning_effort to low/medium/high; xhigh arrived
with the GPT-5.1-Codex-Max generation and the o-series models named in
the docs (o1, o3-mini, o4-mini) 400 on it.
- agent_builder.py: validate reasoning effort against the resolved model
via pydantic-ai's model profile (shared helper
_openai_model_supports_reasoning), mirroring the Anthropic thinking
check, instead of trusting the provider-level tuple for every model.
- openai.py: drop the invented default max_tokens=4096; an output cap on
a reasoning model can be entirely consumed by reasoning tokens and
surfaces as a misleading structured-output retry failure.
- openai.py: validate_connection now mirrors ClaudeProvider's
inconclusive-probe pattern (401/403/connection errors fail, other HTTP
statuses warn and proceed with _connection_probe_note), and reports a
mistyped default model against the fetched model list.
- openai.py: get_model_capabilities no longer uses the startswith("o")
heuristic (it misclassified the provider's own default gpt-5-mini and
any "openai/..."-prefixed OpenRouter id); it consults the pydantic-ai
model profile and returns None for unknowns.
- schema/factory/registry: remove the never-implemented openai-agents
provider name and unify the drifted provider-name Literals behind a
single ProviderName alias in config/schema.py.
- Temperature ceilings are now capability-driven: ProviderCapabilities
gains max_temperature (1.0 for copilot/claude/hermes/aca/
claude-agent-sdk), the static validator reads it instead of hardcoding
provider names, and create_provider enforces it so run/resume are
covered too, restoring the 1.1-for-claude rejection test.
- diagnostics.py: drop OPENAI_BASE_URL from openai's credential spec —
an unset base URL must not render as a missing credential.
- pyproject.toml: widen the openai pin to >=2.48.0,<3.0.0.
- docs/providers/openai.md: remove the unreachable "install the SDK"
instructions and troubleshooting entry, correct the reasoning-effort
matrix (xhigh rejected), and renumber Quick Start.
- CHANGELOG: note the pydantic-ai -> pydantic-ai-slim dependency swap,
the openai-agents removal, and the temperature enforcement move.
- AGENTS.md: restore the Run/Resume Parity tail truncated in a rebase,
and document the new temperature capability.
- tests: new real-API integration test (tests/test_integration/
test_openai_real_api.py, pytest.mark.real_api) exercised against an
OpenAI-compatible endpoint; satisfies the stable-tier real-API bar.
Co-Authored-By: Sisyphus (OhMyOpenCode) <noreply@ohmyopencode.com>
@hertznsk
hertznskforce-pushed the feat/openai-pydantic-provider branch from 0eea251 to a0aa16bCompareAugust 19, 2026 23:52
@hertznsk

Copy link
Copy Markdown
ContributorAuthor

Thanks for the thorough review — every concern checked out against the code. All 14 inline threads are addressed in the replies above and in commit a0aa16b (branch rebased onto current main first, which is also what resolved the retry.py deny-list point — #458 had already landed the allow-list there).

On the two non-inline items:

AGENTS.md truncation — confirmed and fixed. The Run/Resume Parity bullet now ends with the full tail again (dialog_message inertness, replayMode latching in Header.tsx/use-replay.ts, EventLogSubscriber append mode). I verified line-by-line that every bullet present in origin/main's AGENTS.md is present in the branch's version; the rebase artifact is gone.

tier="stable" real-API test — added tests/test_integration/test_openai_real_api.py behind pytest.mark.real_api, mirroring the Claude precedent, and actually executed it against a live OpenAI-compatible endpoint: validate_connection, a plain Q&A run, and a structured-output run all pass end-to-end (2 passed). The test accepts CONDUCTOR_TEST_OPENAI_BASE_URL/CONDUCTOR_TEST_OPENAI_MODEL overrides so it can run against api.openai.com proper in CI with a real key, and against a local gateway otherwise. I kept the tier at stable on the strength of that run plus the HTTP-stub pipeline tests, but if you'd rather it land as experimental and promote after a nightly has burned in, that's a one-line change plus a docs-table edit — happy to do it.

One note on the retry comment: I did not extend _get_retry_after for ModelHTTPError (honoring a 429 Retry-After header on the translated error). That's pre-existing on main, affects Claude identically, and felt like it deserved its own issue rather than a ride-along here.

Jason Robertand others added 2 commits August 22, 2026 14:04
… lint
Follow-up to the review round on the native OpenAI provider. No behaviour
changes; the provider code is untouched apart from two suggestion strings.
- Run `ruff format` on the three files CI flagged. The formatter check was
failing, which skipped every downstream job, so no test run had executed
against this branch head.
- Move the OpenAI provider CHANGELOG entry out of the released `[0.1.28]`
section and into `[Unreleased]`, where it belongs. A rebase had carried it
down as 0.1.29-0.1.33 were inserted above it, so it was editing the notes of
a shipped release while the version that will actually carry the feature had
no entry at all. Also record the retry-classification change (408/429/5xx are
now retried on Claude as well) and the `default_reasoning_effort` forwarding
fix, both of which alter existing provider behaviour.
- Describe the credential rule the provider actually implements. The docs
promised `OPENAI_API_KEY` and `OPENAI_BASE_URL` both work as environment
fallbacks, but once a custom `base_url` is in effect the ambient key is
refused and construction raises. That is the correct behaviour, and it is the
configuration the recipes on the same page recommend, so the page needed to
say so rather than send readers into a `ValidationError`.
- Drop the remaining `xhigh` claims left over from narrowing the capability
tuple to `("low", "medium", "high")`, including an example comment that
contradicted the matrix ten lines above it.
- Correct the comparison table's context-window cell: `get_max_prompt_tokens`
returns `None` unconditionally, so the provider reports no window.
- Point the "install the SDK" suggestions at `openai>=2.48.0` instead of
`>=1.0.0`, two majors below the project's own floor.
- Rewrite the AGENTS.md provider notes so the env-resolution, reasoning-effort
and temperature bullets match the implementation, and record why
`agent_reasoning_events` is `False` on a Chat Completions backend.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves the CHANGELOG conflict: microsoft#420's MCP 2.0 entry and this branch's
OpenAI provider entries both landed in the previously-empty [Unreleased]
section, so git could not merge them. Both are kept, with the two Fixed
items combined under a single heading.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756

Copy link
Copy Markdown
Collaborator

Went back through all fourteen comments against a0aa16b — every one is genuinely addressed, and a few of the fixes are better than what I asked for.

The temperature work is the standout. Moving the ceiling onto ProviderCapabilities.max_temperature and enforcing it in create_provider covers run and resume rather than only validate, and it removes the hardcoded provider name I expected would need editing for the next provider. The reasoning check reading openai_supports_reasoning off the model profile and returning None for unknown is the right shape too, since it skips rather than guesses. I verified the credential fix by construction rather than by reading it: a YAML base_url with an ambient key now raises, and leaving self._api_key as None keeps the builder guard live in production instead of only under its unit test.

The remaining items were small and almost entirely documentation, so rather than send it back around again I pushed them myself in caa80f5. No behaviour changed — the provider code is untouched apart from two suggestion strings.

  • ruff format on the three files CI flagged. The formatter check was failing, which skipped every downstream job, so no test run had actually executed against the branch.
  • Moved the OpenAI CHANGELOG entry out of the released [0.1.28] section into [Unreleased]. A rebase had carried it down as 0.1.29 through 0.1.33 were inserted above it, so it was editing the notes of a shipped release while the version that will carry the feature had nothing. Added the retry-classification and default_reasoning_effort changes while there, since both alter existing provider behaviour.
  • Dropped the leftover xhigh claims from the docs and AGENTS.md, including an example comment that contradicted the effort matrix ten lines above it.
  • Corrected the comparison table's context-window cell. get_max_prompt_tokens returns None unconditionally, so the provider reports no window; that row had inherited Claude's value. This one was mine — it came out of the review but I left it out of the first round, so it was never something you missed.
  • Pointed the two "install the SDK" suggestions at openai>=2.48.0 rather than >=1.0.0.

On the one open question, the OPENAI_BASE_URL case: I resolved it in favour of your implementation rather than loosening it. The docs claimed both environment variables work as fallbacks, but once a custom base_url is in effect the ambient key is refused and construction raises, which is the configuration the recipes on that same page recommend. Rather than reopen the credential path I rewrote the precedence rules to state the stricter behaviour, with a note that the key belongs in YAML via ${OPENAI_API_KEY}. If you would rather allow the paired-environment-variable case, that is a reasonable call and the docs would just move back the other way.

525600c then merges main, which had drifted underneath us. #420's MCP 2.0 entry and this branch's entries both landed in the previously-empty [Unreleased] section, so git could not merge them; both are kept, with the two Fixed items combined. That also brings the branch up to date, and mergeable is back to true.

Locally on the merge result: ruff format, ruff check and ty clean, 7534 passed and 46 skipped, and examples/openai-compatible.yaml validates. CI needed maintainer approval for the fork run, which I have approved; Lint, Type Check, Frontend, Install Scripts, Validate Examples and Web BG Smoke are green and the test matrix is still going.

Thanks for working through a long list carefully. Nothing further from me beyond the base_url question, which is yours to call either way.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.78146% with 78 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@2dbb712). Learn more about missing BASE report.

Files with missing linesPatch %Lines
src/conductor/providers/openai.py77.16%58 Missing ⚠️
src/conductor/providers/_pydantic_ai/runner.py87.09%8 Missing ⚠️
src/conductor/providers/__init__.py0.00%3 Missing ⚠️
src/conductor/providers/factory.py89.28%3 Missing ⚠️
.../conductor/providers/_pydantic_ai/agent_builder.py95.91%2 Missing ⚠️
src/conductor/providers/_pydantic_ai/retry.py75.00%2 Missing ⚠️
src/conductor/config/validator.py92.85%1 Missing ⚠️
src/conductor/providers/diagnostics.py90.90%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #421 +/- ##
=======================================
Coverage ? 91.73% =======================================
Files ? 146 Lines ? 23640 Branches ? 0 =======================================
Hits ? 21686 Misses ? 1954 Partials ? 0 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jrob5756Jason Robert (jrob5756) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Approved!

@jrob5756
Jason Robert (jrob5756) merged commit 3d9b071 into microsoft:mainAug 22, 2026
24 of 25 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Idea: evaluate Pydantic AI as a shared inner agent runtime for direct OpenAI and Anthropic providers

3 participants

@hertznsk@jrob5756@codecov-commenter