Skip to content

fix(llm): OpenAI provider compatibility for caching, temperature, and tool calls - #98

Merged
jkyberneees merged 2 commits into
mainfrom
fix/openai-compat
Jul 25, 2026
Merged

fix(llm): OpenAI provider compatibility for caching, temperature, and tool calls#98
jkyberneees merged 2 commits into
mainfrom
fix/openai-compat

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

What

Request-shape fixes in the LLM client/loop, found by running the agent against the real OpenAI API. DeepSeek tolerates all of these shapes, which is why they went unnoticed until switching providers.

The bugs

1. Anthropic prompt-caching shape sent to every provider — with prompt_caching enabled, the loop moved the system prompt into the Anthropic-only top-level system field and added cache_control markers. OpenAI rejects this: 400 Unknown parameter: 'system'.

2. temperature: 0 sent to reasoning models — odek defaults to temperature 0 for determinism, but OpenAI reasoning models (o1/o3/o4, gpt-5 series) only accept the default (1): 400 'temperature' does not support 0 with this model.

3. Function tools + reasoning_effort rejected by gpt-5.6 models — gpt-5.6-luna/sol reject tools combined with any effort other than "none", and their default effort is not "none", so omitting the field still 400s (verified with raw curl).

4. thinking: enabled/disabled sent the Anthropic object unconditionally — OpenAI rejects the unknown top-level thinking parameter with a 400, so both values were broken against OpenAI.

The fixes

  • internal/loop/loop.go: ApplyCacheMarkers now only runs when Client.IsAnthropic() (new). OpenAI/DeepSeek cache automatically — no markers needed.
  • internal/llm/client.go: modelForbidsTemperature (prefix match on o1/o3/o4/gpt-5) omits the temperature field for reasoning models only; every other provider/model keeps the deterministic temperature: 0 default.
  • internal/llm/client.go: learned fallback — when a 400 names reasoning_effort as the offending parameter on a tool-bearing request, an atomic forceNoneEffort flag is set, the call retries once with reasoning_effort: "none", and subsequent tool calls send it directly (no more failed round-trips). Tool-less calls keep the configured effort.
  • internal/llm/client.go: thinking shaping extracted into buildCallParams and gated per provider via sendsThinkingObject (Anthropic/DeepSeek unchanged). OpenAI reasoning models map enabledreasoning_effort: "high" and disabled"none"; non-reasoning models omit both fields.

Verified live (api.openai.com + api.deepseek.com)

Basic run Tool round-trip thinking: high thinking: disabled thinking: enabled Caching
OpenAI gpt-4o-mini ✅ (integration suite) ✅ automatic
OpenAI gpt-5-nano ✅ automatic
OpenAI gpt-5.6-luna ✅ (math_eval → 391) ✅ automatic
OpenAI gpt-5.6-sol ✅ automatic
DeepSeek v4 Flash (regression) ✅ native metrics intact

DeepSeek regression notes: native cache metrics (cache_creation/cache_read) still flow, and the deterministic temperature: 0 default is still sent for non-reasoning models.

Tests

  • New regression tests: TestClient_IsAnthropic, TestCall_OmitsTemperatureForReasoningModels, TestCall_LearnsReasoningEffortNoneWithTools, TestBuildCallParams_ThinkingMapping (client), TestEngine_PromptCaching_NonAnthropicSkipsMarkers (loop)
  • Two pre-existing thinking tests that encoded the old unconditional behavior now exercise buildCallParams directly
  • go test ./internal/llm/ ./internal/loop/ ./cmd/odek/ -count=1 — all pass

… tool calls

Three request-shape bugs surfaced when running against the real OpenAI
API (DeepSeek tolerates all three, which is why they went unnoticed):

1. Anthropic prompt-caching shape sent to every provider. With
   prompt_caching enabled, the loop moved the system prompt into the
   Anthropic-only top-level "system" field and added cache_control
   markers; OpenAI rejects this with 400 unknown_parameter. Markers are
   now only applied when the endpoint is Anthropic (new
   Client.IsAnthropic). OpenAI and DeepSeek cache automatically.

2. temperature:0 sent to reasoning models. odek defaults to
   temperature 0 for determinism, but OpenAI reasoning models
   (o1/o3/o4, gpt-5 series) only accept the default (1) and answer 400
   unsupported_value. The field is now omitted for those models;
   behavior for all other providers/models is unchanged.

3. Function tools + reasoning_effort rejected by gpt-5.6 models.
   gpt-5.6-luna/sol reject tools combined with any reasoning_effort
   other than "none" — and their default effort is not "none", so
   omitting the field still 400s. The client now learns the constraint
   from the 400 (atomic flag), retries once with effort "none", and
   pins it for subsequent tool-bearing calls; tool-less calls keep the
   configured effort.

Verified live against api.openai.com (gpt-4o-mini, gpt-5-nano,
gpt-5.6-luna, gpt-5.6-sol — basic runs, tool round-trips, automatic
prompt caching) and regression-checked against DeepSeek v4 Flash
(native cache metrics and temperature:0 default intact).

Regression tests: TestClient_IsAnthropic,
TestCall_OmitsTemperatureForReasoningModels,
TestCall_LearnsReasoningEffortNoneWithTools,
TestEngine_PromptCaching_NonAnthropicSkipsMarkers.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 25, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek b94f9e4 Commit Preview URL

Branch Preview URL
Jul 25 2026, 12:23 PM

…s sending the Anthropic object

The thinking object ({"type":"enabled"/"disabled"}) was sent
unconditionally; OpenAI rejects unknown top-level parameters with a
400, so thinking: disabled (and enabled) was broken against OpenAI —
same family as the system-field bug fixed earlier in this branch.

The shaping logic now lives in an extracted, directly testable
buildCallParams method:

- Anthropic/DeepSeek (new sendsThinkingObject gate): unchanged,
  thinking object sent as before.
- OpenAI reasoning models (o1/o3/o4, gpt-5 series): enabled maps to
  reasoning_effort "high", disabled to "none" (verified accepted on
  the gpt-5 series); temperature stays omitted per the forbidlist.
- Non-reasoning models (e.g. gpt-4o): no thinking capability exists,
  so both fields are omitted and temperature is honored as configured.

The two existing thinking tests asserted the old unconditional
behavior through an httptest server whose URL is never a recognized
provider; they now exercise buildCallParams directly. New table test
TestBuildCallParams_ThinkingMapping covers all provider/model/thinking
combinations.
@jkyberneees
jkyberneees merged commit fabbddd into main Jul 25, 2026
8 checks passed
Sign up for free to 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.

1 participant