Skip to content

OpenAI support - #1178

Merged
hanorik merged 14 commits into
mainfrom
openai_support
Jul 23, 2026
Merged

hanorik merged 14 commits into
mainfrom
openai_support

Conversation

@hanorik

@hanorik hanorik commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

This PR duplicates #242 to apply required fixes keeping full ownership and clean history as there's no movement in the original one.

levigross and others added 4 commits November 11, 2025 09:35
This PR enables basic support for OpenAI models (and endpoints that expose
a OpenAI API compatible API interface).

I am stressing on *basic* support because we will leave tool calling to
the next PR :)

@karolpiotrowicz karolpiotrowicz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for picking this up and carrying #242 forward with a clean history — the package structure and the genai↔OpenAI conversion layering are in good shape, and it's close to the adk-python labs/openai implementation.

I built the package locally and verified behavior against the openai-go/v3 and google.golang.org/genai types directly. A heads-up worth calling out up front: CI is green but the tests are fully offline (inline httptest mocks that don't validate the outgoing request), so several real issues aren't exercised by the suite. I found a few blocking correctness bugs in exactly those un-exercised paths:

  • Tool/structured-output JSON schemas are emitted with uppercase types ("OBJECT", "STRING"), which OpenAI rejects.
  • Function tools are always sent strict: true, which 400s on typical genai schemas.
  • Streaming function calls return the item id instead of the call_id, so streamed tool-call round-trips are inconsistent with the non-streaming path.
  • The streaming iterator can panic (yields from a deferred Close()), and streamed responses drop token usage.
  • The package doc example doesn't compile against the current NewModel signature.

Details in the inline comments below (from blockers to nits). Most have small, self-contained fixes, and adk-python is a handy parity reference for a couple of them. Happy to help with any of these. Since it's still a draft, marking as request-changes for tracking rather than as a gate.

Comment thread model/openaimodel/tools.go
Comment thread model/openai/tools.go Outdated
Comment thread model/openai/doc.go Outdated
Comment thread model/openai/stream.go Outdated
Comment thread model/openai/openai.go Outdated
Comment thread model/openaimodel/openai.go
Comment thread model/openai/openai.go Outdated
Comment thread model/openai/response.go Outdated
Comment thread model/openaimodel/openai.go
Comment thread model/openaimodel/request_test.go
hanorik added 2 commits July 21, 2026 13:58
Adds a runnable example showing an ADK llmagent backed by the OpenAI model
from #1178 (openaimodel.NewModel) instead of Gemini. The only difference from
the Gemini quickstart is the model constructor; agents, the functiontool,
the runner, and the launcher are unchanged.

The sample registers a get_weather function tool to exercise OpenAI tool
calling, defaults to gpt-4o-mini, and supports OpenAI-compatible endpoints via
OPENAI_BASE_URL. A README follows the examples/workflow sample layout.
@hanorik
hanorik force-pushed the openai_support branch 2 times, most recently from 066b300 to 8093c15 Compare July 22, 2026 10:17
@karolpiotrowicz

Copy link
Copy Markdown
Contributor

One remaining gap in the structured-output path that I think is worth a look before this lands (the schema type-case handling you added in the last commit already cleared the earlier blocker; this is the next one behind it).

Symptom: requesting structured output — either via LlmAgent's OutputSchema, or directly through GenerateContentConfig.ResponseSchema — fails against OpenAI with a 400, and the run produces no output:

openai: call failed: 400 Bad Request
Invalid schema for response_format 'adk_response':
'additionalProperties' is required to be supplied and to be false.

Minimal repro:

m, _ := openaimodel.NewModel(ctx, "gpt-4o-mini",
	&openaimodel.ClientConfig{APIKey: os.Getenv("OPENAI_API_KEY")})

req := &model.LLMRequest{
	Contents: []*genai.Content{{Role: genai.RoleUser, Parts: []*genai.Part{
		{Text: "Paris is 22C. Extract city and temperature_c."}}}},
	Config: &genai.GenerateContentConfig{
		ResponseMIMEType: "application/json",
		ResponseSchema: &genai.Schema{
			Type: genai.TypeObject,
			Properties: map[string]*genai.Schema{
				"city":          {Type: genai.TypeString},
				"temperature_c": {Type: genai.TypeInteger},
			},
			Required: []string{"city", "temperature_c"},
		},
	},
}
for _, err := range m.GenerateContent(ctx, req, false) {
	// err -> 400 ... 'additionalProperties' is required to be supplied and to be false
}

This also reproduces through a normal agent: basic_processor maps LlmAgent.OutputSchema onto Config.ResponseSchema, so any OpenAI-backed agent with an OutputSchema set hits it (with or without tools).

Root cause: the Responses API json_schema response format requires additionalProperties: false on every object node in the schema, but newJSONSchemaFormat (in model/openaimodel/request.go) doesn't add it.

How adk-python handles it: its native OpenAI Responses integration does exactly this transform in labs/openai/_openai_schema.pyenforce_strict_openai_schema() walks the schema recursively and, for each object, sets additionalProperties = False and populates required (also handling $ref/$defs/anyOf/items). Its LiteLLM path (models/lite_llm.py) applies the same rule. Python opts into OpenAI strict structured outputs, which is why it also fills required.

Possible direction (your call): mirror that helper — recursively inject additionalProperties: false on object nodes in the response-format schema (similar in spirit to the recursive lowercaseSchemaTypes you already have). If you also set strict: true to match Python's behavior, you'd want to populate required for every property too; if you keep the format non-strict, additionalProperties: false alone clears the error and optional fields keep working.

Scope / not affected:

  • Function/tool calling is fine — tool parameter schemas don't hit this.
  • Structured output via a raw ResponseJsonSchema that already includes "additionalProperties": false works, which is a usable workaround in the meantime.

Happy to share the exact schemas I tested if useful. Thanks again!

@karolpiotrowicz

Copy link
Copy Markdown
Contributor

Found a bug in streaming + tool calling that's worth fixing before this lands (it builds on your streaming refactor).

Symptom: with an agent that has a function tool (e.g. the examples/openai weather sample), a tool-calling prompt in SSE streaming mode produces no output. It only works with non-streaming. Running it through the runner in streaming mode, the tool is never invoked — the run yields empty events with no function call.

Root cause: in the OpenAI Responses streaming API, the function name is delivered on the response.output_item.added event. The finalizing response.function_call_arguments.done event does not carry the name — it arrives empty. I confirmed this at runtime:

DONE name=""  itemID="fc_...b161e23705aef078"  args={"city":"Paris"}

emitFunctionCall builds the function call from done.Name (empty), and the shared streaming aggregator drops any completed function call whose name is empty (internal/llminternal/stream_aggregator.go — the if part.FunctionCall.Name != "" guard). So the tool call is silently discarded, the tool never runs, and streaming produces nothing.

Non-streaming is unaffected (the name comes straight from the response output items), and plain-text streaming is unaffected (no function call). The broken combination is specifically streaming + tools.

The output_item.added handling already added in this branch captures the CallID but not the Name, so the issue is still present on the current head.

Proposed fix (mirrors the existing itemToCallID mapping) in model/openaimodel/stream.go:

type streamTranslator struct {
	functionArgs map[string]*strings.Builder
	itemToCallID map[string]string
	itemToName   map[string]string // add
}

// in newStreamTranslator():
itemToName: make(map[string]string),

// in the responseOutputItemAdded case, alongside the CallID capture:
if added.Item.ID != "" && added.Item.Name != "" {
	t.itemToName[added.Item.ID] = added.Item.Name
}

// in emitFunctionCall(), before building the part:
name := done.Name
if name == "" {
	name = t.itemToName[done.ItemID]
}
delete(t.itemToName, done.ItemID)
// ...then use `Name: name` in the returned genai.FunctionCall

With this applied, streaming tool calling works end-to-end — function call → tool executes → final text streams token-by-token:

[1] fc="get_weather"
[3] fr="get_weather"
[4..] text="The" " weather" " in" " Paris" ...
[16] text="The weather in Paris is currently 22°C and sunny."

Might be worth a unit test with a fake stream where the function_call_arguments.done event omits name (and the name only appears on output_item.added), to lock this in. Happy to help if useful — thanks!

@hanorik
hanorik marked this pull request as ready for review July 23, 2026 15:18
@hanorik
hanorik requested a review from karolpiotrowicz July 23, 2026 15:18
@hanorik
hanorik merged commit f4c7670 into main Jul 23, 2026
13 checks passed
PratikDhanave pushed a commit to PratikDhanave/adk-go that referenced this pull request Aug 15, 2026
* Add support for the OpenAPI models

This PR enables basic support for OpenAI models (and endpoints that expose
a OpenAI API compatible API interface).

I am stressing on *basic* support because we will leave tool calling to
the next PR :)

* Run go mod tidy

* refactor: rename package to openaimodel, introduce ClientConfig, and propagate response metadata in streams

* refactor: add support for logprobs conversion and update finish reason handling for OpenAI responses

* refactor: replace raw error strings with sentinel errors and add comprehensive unit test coverage for OpenAI model converters

* docs(examples/openai): add OpenAI model integration sample

Adds a runnable example showing an ADK llmagent backed by the OpenAI model
from google#1178 (openaimodel.NewModel) instead of Gemini. The only difference from
the Gemini quickstart is the model constructor; agents, the functiontool,
the runner, and the launcher are unchanged.

The sample registers a get_weather function tool to exercise OpenAI tool
calling, defaults to gpt-4o-mini, and supports OpenAI-compatible endpoints via
OPENAI_BASE_URL. A README follows the examples/workflow sample layout.

* refactor: move openai model package to openaimodel and reorganize internal structure

* feat: enable strict mode for OpenAI structured outputs by enforcing schema requirements

* docs: mark openaimodel package as experimental

* fix: track function names in stream translator to handle missing names in done events and cleanup unused schema test code.

---------
@hanorik
hanorik deleted the openai_support branch August 23, 2026 17:38
houzhonglogic pushed a commit to Seek-Key-LTD/key-agent that referenced this pull request Sep 13, 2026
* Add support for the OpenAPI models

This PR enables basic support for OpenAI models (and endpoints that expose
a OpenAI API compatible API interface).

I am stressing on *basic* support because we will leave tool calling to
the next PR :)

* Run go mod tidy

* refactor: rename package to openaimodel, introduce ClientConfig, and propagate response metadata in streams

* refactor: add support for logprobs conversion and update finish reason handling for OpenAI responses

* refactor: replace raw error strings with sentinel errors and add comprehensive unit test coverage for OpenAI model converters

* docs(examples/openai): add OpenAI model integration sample

Adds a runnable example showing an ADK llmagent backed by the OpenAI model
from google#1178 (openaimodel.NewModel) instead of Gemini. The only difference from
the Gemini quickstart is the model constructor; agents, the functiontool,
the runner, and the launcher are unchanged.

The sample registers a get_weather function tool to exercise OpenAI tool
calling, defaults to gpt-4o-mini, and supports OpenAI-compatible endpoints via
OPENAI_BASE_URL. A README follows the examples/workflow sample layout.

* refactor: move openai model package to openaimodel and reorganize internal structure

* feat: enable strict mode for OpenAI structured outputs by enforcing schema requirements

* docs: mark openaimodel package as experimental

* fix: track function names in stream translator to handle missing names in done events and cleanup unused schema test code.

---------

Co-authored-by: Levi Gross <levi@levigross.com>
Co-authored-by: Levi Gross <levigross@users.noreply.github.com>
Co-authored-by: wolo <wolo@google.com>
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.

5 participants