From 499347dfd533cc01be73326252f2076ebd539d35 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 31 Aug 2026 16:53:51 +0200 Subject: [PATCH 1/2] Upgrade OpenAI Go SDK to v3.54.0 --- go.mod | 2 +- go.sum | 32 +++++++- provider/foundryprovider/agent.go | 92 +++++++++++------------ provider/foundryprovider/agent_test.go | 5 +- provider/openaiprovider/responses.go | 18 +++-- provider/openaiprovider/responses_test.go | 4 +- 6 files changed, 95 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 020ccbca..0f8e108d 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 github.com/modelcontextprotocol/go-sdk v1.7.0 - github.com/openai/openai-go/v3 v3.52.0 + github.com/openai/openai-go/v3 v3.54.0 go.opentelemetry.io/otel v1.45.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.45.0 go.opentelemetry.io/otel/metric v1.45.0 diff --git a/go.sum b/go.sum index 3f868819..d6f3a833 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,34 @@ github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20260312103001-8e7ab1df github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20260312103001-8e7ab1df34c8/go.mod h1:ERAMOexUee4AIuoxksuuGoEcHl3aqLwaazjGwlR9ZCI= github.com/anthropics/anthropic-sdk-go v1.66.0 h1:/CKwgscn0Pe1q4U8aFInSOt/v06JeMc9Aq4vIlctCFw= github.com/anthropics/anthropic-sdk-go v1.66.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI= +github.com/aws/aws-sdk-go-v2 v1.43.6 h1:RrmFcqCBxkJuf7g1axVo5krB4jM/AO8r5e5oujrgdoQ= +github.com/aws/aws-sdk-go-v2 v1.43.6/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= +github.com/aws/aws-sdk-go-v2/config v1.32.37 h1:Ljl7LOJB6ym0liuEl0+TZ3d7f5I8MEZN1Cj9PINlj/g= +github.com/aws/aws-sdk-go-v2/config v1.32.37/go.mod h1:WJ7pe7ZPpmG8Q5kKS53zeypIV4FBGACxmte8Uc6SgUc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.36 h1:84s5xMme6ENYEdKG8rsbSFFg/8+lbHBeM9QYSO0gnDk= +github.com/aws/aws-sdk-go-v2/credentials v1.19.36/go.mod h1:c46BLdagDLIswjgt+GeQOslXgeS0E6wCacs5yZbxPGk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37 h1:b5tb+CZItBkydC7r3hTNdSO3pszG1R2EtnA+7TePQPk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.37/go.mod h1:ZQ+6SU9X0oz6+7MUCSswv9Mjci4eaqZr21HI2RVy/yA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37 h1:lznzIOvvbqjfe8UAaciCRJgBgJsxuTROKlhZuXQWfv8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.37/go.mod h1:otfkzyfQeMMLZAqX59GSXTL3o22BR/l6HFaRzzbWSqA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37 h1:zCEORWo0eU0gDjG+IyApE/2B+ZGG1m+GU7B263XV8ds= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.37/go.mod h1:i6c0PEl3TNOWxRbQ++KQcVenPWS/GoQeiklKhNuqzJ8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38 h1:A3UAuCmx7LyUcrixBTzKJYYIUZ2yTvn6ZhT8PB+7APk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38/go.mod h1:1PDUYG9Z+JrbbsobsAZHjWOm9QBT/djiK3QbykTL5Z4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37 h1:a3D4AjrOrTrP8+d9ILBthqrElf0z1JNol09Xvnwcys8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.37/go.mod h1:ky0gTu+ukvUTuUKFIpp6Wid4oninrkCyvbFkVs0kpHM= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.6 h1:i68sFvXidKlkiSvI7d7Ilc1/UvW4CtBOaivH7jhG4fs= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.6/go.mod h1:/h7Obr9WTtzbjTHGASRQwLN7Bupw+TC3x8x7fyx39hE= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.6 h1:tpfGChmjUmv3W9WlRvy+stwKDTbFFdq8Zk9DbFPrfMU= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.6/go.mod h1:CSjiDzmG/lsKkTOYjbkM+duLmRlW+LOxD64Na44ijnI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6 h1:49BBtY68A+KJCQ3a2F3eUe6ROsKucxUdfHKoqorc0wI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.6/go.mod h1:ptG2hbs7QltE1GcQY0MpS4bfrc51KCnBXUr7OT1EEfE= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.6 h1:JvExZWabChDM0qJAirQYGfOYo0ndT3edXj+fqSPNjkE= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.6/go.mod h1:XZcaQkV2cItp6yEkrwljyaPOf22RuX7T43jxap/FOmM= +github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY= +github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= @@ -74,8 +102,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= -github.com/openai/openai-go/v3 v3.52.0 h1:VDSjIvI5Sr2/AzGJI6219sM2Il+zBWuopvluMy6KdjE= -github.com/openai/openai-go/v3 v3.52.0/go.mod h1:Vy3y2/I2H/MbqvJGXEK8VbN5+avZV6zxux4I3eBdvaA= +github.com/openai/openai-go/v3 v3.54.0 h1:qeNvIpFB/wzX7pI8USN1eruvzCS+XqOZ8mx73lGF6Lg= +github.com/openai/openai-go/v3 v3.54.0/go.mod h1:ufI1+K+t0ijRB3gk8eztiw1crcDpsBuxRQL4sbLIrts= github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= diff --git a/provider/foundryprovider/agent.go b/provider/foundryprovider/agent.go index 79f508d6..8543435c 100644 --- a/provider/foundryprovider/agent.go +++ b/provider/foundryprovider/agent.go @@ -3,7 +3,9 @@ package foundryprovider import ( + "cmp" "fmt" + "net/http" "net/url" "strings" @@ -59,17 +61,20 @@ func NewAgent(endpoint string, credential azcore.TokenCredential, target AgentTa if credential == nil { panic("credential is required") } + var baseURL, model string + var targetOptions []option.RequestOption + instructions := config.Instructions switch target := target.(type) { case ModelDeployment: projectEndpoint := normalizeAbsoluteEndpoint(endpoint) - model := strings.TrimSpace(string(target)) + model = strings.TrimSpace(string(target)) if model == "" { panic("model is required") } - return newFoundryAgent(credential, config, foundryAgentMode{ - baseURL: projectOpenAIBaseURL(projectEndpoint), - model: model, - }) + baseURL = projectOpenAIBaseURL(projectEndpoint) + // Foundry project endpoints encode the API version in /openai/v1 and + // reject the api-version query added by azure.WithEndpoint. + targetOptions = append(targetOptions, option.WithQueryDel("api-version")) case ServerAgent: projectEndpoint := normalizeAbsoluteEndpoint(endpoint) agentName := strings.TrimSpace(string(target)) @@ -77,14 +82,45 @@ func NewAgent(endpoint string, credential azcore.TokenCredential, target AgentTa panic("agent name is required") } agentEndpoint := serverAgentEndpoint(projectEndpoint, agentName) - return newFoundryAgent(credential, config, foundryAgentMode{ - baseURL: serverAgentOpenAIBaseURL(agentEndpoint), - agentName: agentName, - requestOptions: []option.RequestOption{option.WithQuery("api-version", foundryDataPlaneAPIVersion)}, - }) + baseURL = serverAgentOpenAIBaseURL(agentEndpoint) + config.ID = cmp.Or(config.ID, agentName) + config.Name = cmp.Or(config.Name, agentName) + instructions = "" default: panic(fmt.Sprintf("unsupported Foundry agent target %T", target)) } + + openAIOptions := []option.RequestOption{ + // WithTokenCredential requires the Azure endpoint marker registered by + // WithEndpoint. WithEndpoint also adds ?api-version=v1 and rewrites a + // Responses path from /responses to /openai/responses. + azure.WithEndpoint(baseURL, foundryDataPlaneAPIVersion), + // Keep the complete Foundry OpenAI-compatible route as the request root. + option.WithBaseURL(baseURL), + option.WithMiddleware(func(req *http.Request, next option.MiddlewareNext) (*http.Response, error) { + // Undo WithEndpoint's Azure OpenAI path prefix because baseURL already + // contains the complete Foundry route. Update RawPath as well so escaped + // server agent names remain encoded. + req.URL.Path = strings.TrimPrefix(req.URL.Path, "/openai") + req.URL.RawPath = strings.TrimPrefix(req.URL.RawPath, "/openai") + return next(req) + }), + // Use the Foundry audience while retaining the SDK's token refresh and + // authenticated-transport protections. + azure.WithTokenCredential(credential, azure.WithTokenCredentialScopes([]string{azureAIResourceScope})), + } + openAIOptions = append(openAIOptions, targetOptions...) + openAIOptions = append(openAIOptions, config.OpenAIOptions...) + openAIOptions = append(openAIOptions, clientHeadersRequestOption()) + openAIOptions = append(openAIOptions, servedModelRequestOption()) + config.Middlewares = append([]agent.Middleware{clientHeadersMiddleware{}, servedModelMiddleware{}}, config.Middlewares...) + + return openaiprovider.NewResponsesAgent(openai.NewClient(openAIOptions...), openaiprovider.AgentConfig{ + Config: config.Config, + Instructions: instructions, + Model: model, + DisableStoreOutput: config.DisableStoreOutput, + }) } func serverAgentOpenAIBaseURL(agentEndpoint string) string { @@ -103,42 +139,6 @@ func serverAgentEndpoint(projectEndpoint string, agentName string) string { return endpoint } -type foundryAgentMode struct { - baseURL string - agentName string - model string - requestOptions []option.RequestOption -} - -func newFoundryAgent(credential azcore.TokenCredential, config AgentConfig, mode foundryAgentMode) *agent.Agent { - if config.ID == "" { - config.ID = mode.agentName - } - if config.Name == "" { - config.Name = mode.agentName - } - instructions := config.Instructions - if mode.agentName != "" { - instructions = "" - } - openAIOptions := []option.RequestOption{ - option.WithBaseURL(mode.baseURL), - azure.WithTokenCredential(credential, azure.WithTokenCredentialScopes([]string{azureAIResourceScope})), - } - openAIOptions = append(openAIOptions, mode.requestOptions...) - openAIOptions = append(openAIOptions, config.OpenAIOptions...) - openAIOptions = append(openAIOptions, clientHeadersRequestOption()) - openAIOptions = append(openAIOptions, servedModelRequestOption()) - config.Middlewares = append([]agent.Middleware{clientHeadersMiddleware{}, servedModelMiddleware{}}, config.Middlewares...) - - return openaiprovider.NewResponsesAgent(openai.NewClient(openAIOptions...), openaiprovider.AgentConfig{ - Config: config.Config, - Instructions: instructions, - Model: mode.model, - DisableStoreOutput: config.DisableStoreOutput, - }) -} - func normalizeAbsoluteEndpoint(rawEndpoint string) string { rawEndpoint = strings.TrimSpace(rawEndpoint) if rawEndpoint == "" { diff --git a/provider/foundryprovider/agent_test.go b/provider/foundryprovider/agent_test.go index 2d4340f2..fbdae96b 100644 --- a/provider/foundryprovider/agent_test.go +++ b/provider/foundryprovider/agent_test.go @@ -17,6 +17,9 @@ func TestNewAgentUsesProjectResponsesEndpointAndConfig(t *testing.T) { if r.URL.Path != "/projects/proj/openai/v1/responses" { t.Fatalf("path = %q", r.URL.Path) } + if got := r.URL.Query().Get("api-version"); got != "" { + t.Fatalf("api-version = %q, want omitted", got) + } body := jsonMap(t, mustReadBody(t, r)) if body["model"] != "gpt-4o-mini" { t.Fatalf("model = %#v", body["model"]) @@ -92,7 +95,7 @@ func TestNewAgentRunsAgainstFoundryAgentEndpoint(t *testing.T) { if got := r.Header.Get("Authorization"); got == "" { t.Fatal("missing Authorization header") } - if got := r.Header.Get("User-Agent"); !strings.HasPrefix(got, "agent-framework-go/") { + if got := r.Header.Get("User-Agent"); !strings.Contains(got, "agent-framework-go/") { t.Fatalf("User-Agent = %q", got) } body := jsonMap(t, mustReadBody(t, r)) diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index f308e4d4..96b45c17 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -474,6 +474,12 @@ func responsesDisableStoreOutput(config AgentConfig, opts []agent.Option) bool { return config.DisableStoreOutput } +func responseInputItemParamOfFunctionCallOutput[T string | responses.ResponseFunctionCallOutputItemListParam](callID string, output T) responses.ResponseInputItemUnionParam { + item := responses.ResponseInputItemParamOfFunctionCallOutput(output) + item.OfFunctionCallOutput.CallID = param.NewOpt(callID) + return item +} + // responsesBuildMessageParam converts an agent.Message to one or more OpenAI message parameters. // Returns a slice because some agent messages (like RoleTool) need to be split into multiple OpenAI messages. func responsesBuildMessageParam(msg *message.Message, resp responses.ResponseInputParam) (responses.ResponseInputParam, error) { @@ -607,19 +613,19 @@ func responsesBuildMessageParam(msg *message.Message, resp responses.ResponseInp if funcResult.Error != nil { // Error case - serialize as text with "Error: " prefix - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, fmt.Sprintf("Error: %v", funcResult.Error), )) } else if b, ok := ret.(json.RawMessage); ok { // json.RawMessage - pass as string directly - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, string(b), )) } else if str, ok := ret.(string); ok { // Plain string - pass directly - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, str, )) @@ -700,7 +706,7 @@ func responsesBuildMessageParam(msg *message.Message, resp responses.ResponseInp }, } } - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, outputContent, )) @@ -771,14 +777,14 @@ func responsesBuildMessageParam(msg *message.Message, resp responses.ResponseInp }) } } - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, outputContent, )) } else { // Default case - convert to string (JSON-encode structured // results rather than rendering them with Go's %v). - resp = append(resp, responses.ResponseInputItemParamOfFunctionCallOutput( + resp = append(resp, responseInputItemParamOfFunctionCallOutput( funcResult.CallID, toolResultText(ret), )) diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index 770deb47..f7d3d4c8 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -2519,7 +2519,7 @@ func TestResponsesNonStreamingMCPCall_MapsResultContent(t *testing.T) { "name":"create_issue", "arguments":"{\"title\":\"Bug\"}", "output":"issue #7 created", - "error":"rate limited" + "error":{"type":"mcp_tool_execution_error","message":"rate limited"} }] } ` @@ -2587,7 +2587,7 @@ func TestResponsesStreamingMCPCall_MapsResultContent(t *testing.T) { data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} event: response.output_item.done -data: {"type":"response.output_item.done","sequence_number":1,"output_index":0,"item":{"type":"mcp_call","id":"mcp_123","server_label":"github","name":"create_issue","arguments":"{\"title\":\"Bug\"}","output":"issue #7 created","error":"rate limited"}} +data: {"type":"response.output_item.done","sequence_number":1,"output_index":0,"item":{"type":"mcp_call","id":"mcp_123","server_label":"github","name":"create_issue","arguments":"{\"title\":\"Bug\"}","output":"issue #7 created","error":{"type":"mcp_tool_execution_error","message":"rate limited"}}} event: response.completed data: {"type":"response.completed","sequence_number":2,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"completed","model":"gpt-4o-mini","output":[]}} From 009bf438fd7fbd69e29986395d7c172b76cc97b7 Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Mon, 31 Aug 2026 17:28:08 +0200 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- provider/foundryprovider/agent.go | 2 +- provider/openaiprovider/responses.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/provider/foundryprovider/agent.go b/provider/foundryprovider/agent.go index 8543435c..dde9e190 100644 --- a/provider/foundryprovider/agent.go +++ b/provider/foundryprovider/agent.go @@ -109,8 +109,8 @@ func NewAgent(endpoint string, credential azcore.TokenCredential, target AgentTa // authenticated-transport protections. azure.WithTokenCredential(credential, azure.WithTokenCredentialScopes([]string{azureAIResourceScope})), } - openAIOptions = append(openAIOptions, targetOptions...) openAIOptions = append(openAIOptions, config.OpenAIOptions...) + openAIOptions = append(openAIOptions, targetOptions...) openAIOptions = append(openAIOptions, clientHeadersRequestOption()) openAIOptions = append(openAIOptions, servedModelRequestOption()) config.Middlewares = append([]agent.Middleware{clientHeadersMiddleware{}, servedModelMiddleware{}}, config.Middlewares...) diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 96b45c17..ab600d02 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -476,7 +476,9 @@ func responsesDisableStoreOutput(config AgentConfig, opts []agent.Option) bool { func responseInputItemParamOfFunctionCallOutput[T string | responses.ResponseFunctionCallOutputItemListParam](callID string, output T) responses.ResponseInputItemUnionParam { item := responses.ResponseInputItemParamOfFunctionCallOutput(output) - item.OfFunctionCallOutput.CallID = param.NewOpt(callID) + if callID != "" { + item.OfFunctionCallOutput.CallID = param.NewOpt(callID) + } return item }