Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import (
)

// DefaultAgentAPIVersion is the default API version for agent operations.
const DefaultAgentAPIVersion = "2025-11-15-preview"

// ConversationsAPIVersion is the API version used by the Foundry Conversations protocol.
const ConversationsAPIVersion = "v1"
const DefaultAgentAPIVersion = agent_api.AgentEndpointAPIVersion

// AgentContext holds the common properties of a hosted agent.
type AgentContext struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const agentEndpointHint = "run `azd ai agent show` to see the agent endpoint URL
//
// [1] project name (URL-escaped),
// [2] agent name (URL-escaped),
// [3] protocol tail ("invocations" or "openai/responses").
// [3] protocol tail ("invocations" or "openai/v1/responses").
var agentEndpointPathRegex = regexp.MustCompile(
`^/api/projects/([^/]+)/agents/([^/]+)/endpoint/protocols/(invocations|openai/responses)/?$`,
`^/api/projects/([^/]+)/agents/([^/]+)/endpoint/protocols/(invocations|openai/v1/responses)/?$`,
)

// parsedAgentEndpoint describes a deployed agent invocation endpoint.
Expand All @@ -47,7 +47,7 @@ type parsedAgentEndpoint struct {
// Accepted shapes:
//
// https://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/invocations[?api-version=…]
// https://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/openai/responses[?api-version=…]
// https://<acct>.services.ai.azure.com/api/projects/<proj>/agents/<name>/endpoint/protocols/openai/v1/responses
//
// The host must be a `*.services.ai.azure.com` Foundry host. The path must include the
// protocol-specific suffix; the protocol is derived from the URL.
Expand Down Expand Up @@ -131,7 +131,7 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
switch protocolTail {
case "invocations":
protocol = agent_api.AgentProtocolInvocations
case "openai/responses":
case "openai/v1/responses":
protocol = agent_api.AgentProtocolResponses
}

Expand Down Expand Up @@ -160,12 +160,11 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
}, nil
}

// buildResponsesURL builds the Foundry "openai/responses" protocol URL for an agent.
// apiVersion is URL-encoded so unusual characters cannot break out of the query value.
func buildResponsesURL(projectEndpoint, agentName, apiVersion string) string {
// buildResponsesURL builds the Foundry "openai/v1/responses" protocol URL for an agent.
func buildResponsesURL(projectEndpoint, agentName string) string {
return fmt.Sprintf(
"%s/agents/%s/endpoint/protocols/openai/responses?api-version=%s",
projectEndpoint, agentName, url.QueryEscape(apiVersion),
"%s/agents/%s/endpoint/protocols/openai/v1/responses",
projectEndpoint, agentName,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ func TestParseAgentEndpoint(t *testing.T) {
}{
{
name: "invocations with api-version",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=2025-11-15-preview",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=v1",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "hello",
wantProto: agent_api.AgentProtocolInvocations,
wantAPIVer: "2025-11-15-preview",
wantAPIVer: "v1",
},
{
name: "invocations without api-version",
Expand All @@ -38,12 +38,11 @@ func TestParseAgentEndpoint(t *testing.T) {
wantProto: agent_api.AgentProtocolInvocations,
},
{
name: "responses (openai/responses)",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/responses?api-version=2025-11-15-preview",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "echo",
wantProto: agent_api.AgentProtocolResponses,
wantAPIVer: "2025-11-15-preview",
name: "responses (openai/v1/responses)",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/v1/responses",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "echo",
wantProto: agent_api.AgentProtocolResponses,
},
{
name: "trailing slash tolerated",
Expand Down Expand Up @@ -177,7 +176,7 @@ func TestParseAgentEndpoint_RejectsInvalidAgentNames(t *testing.T) {
for _, name := range cases {
t.Run(name, func(t *testing.T) {
endpoint := "https://acct.services.ai.azure.com/api/projects/proj/agents/" +
name + "/endpoint/protocols/invocations?api-version=2025-11-15-preview"
name + "/endpoint/protocols/invocations?api-version=v1"
_, err := parseAgentEndpoint(endpoint)
if err == nil {
t.Fatalf("parseAgentEndpoint(%q) = nil, want error", name)
Expand All @@ -186,43 +185,37 @@ func TestParseAgentEndpoint_RejectsInvalidAgentNames(t *testing.T) {
}
}

// TestBuildResponsesURL verifies that the responses URL builder uses the parsed
// api-version (rather than the default fallback) and URL-encodes it.
// TestBuildResponsesURL verifies that the responses URL builder uses the
// openai/v1 path with no api-version query parameter.
func TestBuildResponsesURL(t *testing.T) {
t.Parallel()
parsed, err := parseAgentEndpoint(
"https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/responses?api-version=2025-11-15-preview",
"https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/v1/responses",
)
if err != nil {
t.Fatalf("parseAgentEndpoint: %v", err)
}
got := buildResponsesURL(parsed.ProjectEndpoint, parsed.AgentName, parsed.APIVersion)
want := "https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/responses?api-version=2025-11-15-preview"
got := buildResponsesURL(parsed.ProjectEndpoint, parsed.AgentName)
want := "https://acct.services.ai.azure.com/api/projects/proj/agents/echo/endpoint/protocols/openai/v1/responses"
if got != want {
t.Errorf("buildResponsesURL = %q, want %q", got, want)
}

// api-version must be query-escaped so unusual characters cannot break out.
gotEscaped := buildResponsesURL("https://acct.services.ai.azure.com/api/projects/proj", "echo", "weird value&x=1")
if !strings.Contains(gotEscaped, "api-version=weird+value%26x%3D1") {
t.Errorf("buildResponsesURL did not escape api-version: %q", gotEscaped)
}
}

// TestBuildInvocationsURL verifies that the invocations URL builder propagates
// the parsed api-version, URL-encodes it, and URL-encodes any session id.
func TestBuildInvocationsURL(t *testing.T) {
t.Parallel()
parsed, err := parseAgentEndpoint(
"https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=2025-11-15-preview",
"https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=custom-version",
)
if err != nil {
t.Fatalf("parseAgentEndpoint: %v", err)
}

t.Run("no session id", func(t *testing.T) {
got := buildInvocationsURL(parsed.ProjectEndpoint, parsed.AgentName, parsed.APIVersion, "")
want := "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=2025-11-15-preview"
want := "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/invocations?api-version=custom-version"
if got != want {
t.Errorf("buildInvocationsURL = %q, want %q", got, want)
}
Expand Down Expand Up @@ -265,8 +258,8 @@ func TestResolveRemoteContext_EphemeralMode(t *testing.T) {
{
name: "api-version omitted falls back to default",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/" +
"hello/endpoint/protocols/openai/responses",
wantAPIVersion: DefaultAgentAPIVersion,
"hello/endpoint/protocols/openai/v1/responses",
wantAPIVersion: "v1",
wantName: "hello",
wantProject: "https://acct.services.ai.azure.com/api/projects/proj",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func runIdentityCheck(t *testing.T, deps Dependencies, prior []Result) Result {
deps.AzdClient = &azdext.AzdClient{}
}
if deps.AgentAPIVersion == "" {
deps.AgentAPIVersion = "2025-11-15-preview"
deps.AgentAPIVersion = "v1"
}
if deps.readProjectResourceIDFn == nil {
deps.readProjectResourceIDFn = func(_ context.Context, _ *azdext.AzdClient) (string, error) {
Expand Down Expand Up @@ -441,7 +441,7 @@ func TestCheckAgentIdentityRoles_RedactedDetailsDoNotLeakIdentifiers(t *testing.
return agentIdentityProbeResult{PrincipalID: rawPrincipal, StatusCode: 200}
},
queryAgentIdentityRoles: makeQueryReturning(canned),
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
})
resU := check.Fn(t.Context(), Options{Unredacted: true}, prior)
detailsUnredacted := flattenDetails(resU.Details)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func runCheckWithDeps(t *testing.T, deps Dependencies, prior []Result) Result {
deps.AzdClient = &azdext.AzdClient{}
}
if deps.AgentAPIVersion == "" {
deps.AgentAPIVersion = "2025-11-15-preview"
deps.AgentAPIVersion = "v1"
}
c := newCheckAgentStatus(deps)
require.NotNil(t, c.Fn, "newCheckAgentStatus must return a non-nil Fn")
Expand Down Expand Up @@ -202,7 +202,7 @@ func TestCheckAgentStatus_SkipsWhenEndpointMissingFromUpstream(t *testing.T) {

func TestCheckAgentStatus_SkipsWhenAgentServiceListMissingFromUpstream(t *testing.T) {
t.Parallel()
deps := Dependencies{AzdClient: &azdext.AzdClient{}, AgentAPIVersion: "2025-11-15-preview"}
deps := Dependencies{AzdClient: &azdext.AzdClient{}, AgentAPIVersion: "v1"}
prior := []Result{
{ID: "local.environment-selected", Status: StatusPass},
// agent-service-detected passed but didn't surface the list:
Expand Down Expand Up @@ -769,7 +769,7 @@ func TestMakeRealProbeAgentStatus_ReturnsNonNilCloser(t *testing.T) {
// but we can pin the factory: it must return a non-nil closure that
// surfaces a credential-creation error or a network error rather
// than panicking when called.
probe := makeRealProbeAgentStatus("2025-11-15-preview")
probe := makeRealProbeAgentStatus("v1")
require.NotNil(t, probe)
// Invoking with an obviously-invalid endpoint should still
// produce a structured result (not a panic). We pass a very
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (

// foundryProbeStub builds a Dependencies whose probeFoundryEndpoint
// seam returns a fixed foundryProbeResult and an AgentAPIVersion of
// `2025-11-15-preview`. Centralised so every status-code test reads
// `v1`. Centralised so every status-code test reads
// at the same level of abstraction.
func foundryProbeStub(res foundryProbeResult) Dependencies {
return Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
probeFoundryEndpoint: func(_ context.Context, _ string) foundryProbeResult {
return res
},
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestCheckFoundryEndpoint_PassesOn200(t *testing.T) {
endpoint := "https://acct.services.ai.azure.com/api/projects/proj"
check := newCheckFoundryEndpoint(foundryProbeStub(foundryProbeResult{
statusCode: http.StatusOK,
requestedURL: endpoint + "/agents?api-version=2025-11-15-preview&limit=1",
requestedURL: endpoint + "/agents?api-version=v1&limit=1",
}))

got := check.Fn(t.Context(), Options{}, passingPriors(endpoint))
Expand Down Expand Up @@ -278,7 +278,7 @@ func TestCheckFoundryEndpoint_SkipsOnUserCancellation(t *testing.T) {

endpoint := "https://acct.services.ai.azure.com/api/projects/proj"
check := newCheckFoundryEndpoint(Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
probeFoundryEndpoint: func(ctx context.Context, _ string) foundryProbeResult {
<-ctx.Done()
return foundryProbeResult{err: ctx.Err()}
Expand All @@ -298,7 +298,7 @@ func TestCheckFoundryEndpoint_FailsOnProbeTimeout(t *testing.T) {

endpoint := "https://acct.services.ai.azure.com/api/projects/proj"
check := newCheckFoundryEndpoint(Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
probeFoundryEndpoint: func(_ context.Context, _ string) foundryProbeResult {
return foundryProbeResult{err: context.DeadlineExceeded}
},
Expand All @@ -322,7 +322,7 @@ func TestCheckFoundryEndpoint_FallsBackToRealProbeWhenSeamMissing(t *testing.T)
// regardless of the host's network state, and assert the
// cancellation classification kicks in.
check := newCheckFoundryEndpoint(Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
})

ctx, cancel := context.WithCancel(t.Context())
Expand Down Expand Up @@ -360,7 +360,7 @@ func TestRealProbeFoundryEndpoint_RequestShapeAgainstHTTPTestServer(t *testing.T
}))
defer srv.Close()

got, err := buildFoundryProbeURL(srv.URL, "2025-11-15-preview")
got, err := buildFoundryProbeURL(srv.URL, "v1")
require.NoError(t, err)

req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, got, nil)
Expand All @@ -373,7 +373,7 @@ func TestRealProbeFoundryEndpoint_RequestShapeAgainstHTTPTestServer(t *testing.T

require.Equal(t, "/agents", seenPath,
"the built URL must resolve to /agents on the wire")
require.Contains(t, seenQuery, "api-version=2025-11-15-preview")
require.Contains(t, seenQuery, "api-version=v1")
require.Contains(t, seenQuery, "limit=1",
"the probe must use limit=1 (matches production "+
"agent_api/operations.go) — not $top=1")
Expand All @@ -395,7 +395,7 @@ func TestBuildFoundryProbeURL(t *testing.T) {
endpoint: "https://x.services.ai.azure.com/api/projects/proj",
wantContains: []string{
"https://x.services.ai.azure.com/api/projects/proj/agents?",
"api-version=2025-11-15-preview",
"api-version=v1",
"limit=1",
},
},
Expand All @@ -412,7 +412,7 @@ func TestBuildFoundryProbeURL(t *testing.T) {
endpoint: "https://x.services.ai.azure.com/api/projects/proj?api-version=evil&injected=x",
wantContains: []string{
"/api/projects/proj/agents?",
"api-version=2025-11-15-preview",
"api-version=v1",
"limit=1",
},
wantMissing: []string{"api-version=evil", "injected=x"},
Expand All @@ -422,7 +422,7 @@ func TestBuildFoundryProbeURL(t *testing.T) {
endpoint: "https://x.services.ai.azure.com/api/projects/proj#evil/agents",
wantContains: []string{
"/api/projects/proj/agents?",
"api-version=2025-11-15-preview",
"api-version=v1",
"limit=1",
},
wantMissing: []string{"#"},
Expand All @@ -439,7 +439,7 @@ func TestBuildFoundryProbeURL(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := buildFoundryProbeURL(tc.endpoint, "2025-11-15-preview")
got, err := buildFoundryProbeURL(tc.endpoint, "v1")
require.NoError(t, err)
for _, sub := range tc.wantContains {
require.Containsf(t, got, sub, "URL %q missing substring %q", got, sub)
Expand Down Expand Up @@ -481,7 +481,7 @@ func TestBuildFoundryProbeURL_RejectsNonHTTPSOrMalformedEndpoint(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := buildFoundryProbeURL(tc.endpoint, "2025-11-15-preview")
_, err := buildFoundryProbeURL(tc.endpoint, "v1")
require.Error(t, err,
"builder must reject non-HTTPS / relative / malformed "+
"endpoints so the probe never sends a bearer token "+
Expand Down Expand Up @@ -540,7 +540,7 @@ func TestCheckFoundryEndpoint_FailsOnNonHTTPSEndpoint(t *testing.T) {
// the request at validation time, BEFORE any token is acquired
// or any probe is dispatched.
check := newCheckFoundryEndpoint(Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
probeFoundryEndpoint: func(_ context.Context, _ string) foundryProbeResult {
t.Fatal("probe must not be invoked for a non-HTTPS endpoint")
return foundryProbeResult{}
Expand All @@ -561,7 +561,7 @@ func TestCheckFoundryEndpoint_FailsOnMalformedEndpoint(t *testing.T) {
t.Parallel()

check := newCheckFoundryEndpoint(Dependencies{
AgentAPIVersion: "2025-11-15-preview",
AgentAPIVersion: "v1",
probeFoundryEndpoint: func(_ context.Context, _ string) foundryProbeResult {
t.Fatal("probe must not be invoked for a malformed endpoint")
return foundryProbeResult{}
Expand Down Expand Up @@ -648,7 +648,7 @@ func TestFoundryDetails_IncludesStatusAndURLWhenSet(t *testing.T) {

d := foundryDetails("https://x", foundryProbeResult{
statusCode: 200,
requestedURL: "https://x/agents?api-version=2025-11-15-preview&limit=1",
requestedURL: "https://x/agents?api-version=v1&limit=1",
})
require.Equal(t, 200, d["statusCode"])
require.Contains(t, d["requestedURL"], "/agents")
Expand All @@ -665,7 +665,7 @@ func TestFoundryDetails_NeverContainsToken(t *testing.T) {
// refuse to surface it.
d := foundryDetails("https://x", foundryProbeResult{
statusCode: 200,
requestedURL: "https://x/agents?api-version=2025-11-15-preview",
requestedURL: "https://x/agents?api-version=v1",
})
for k, v := range d {
require.NotContains(t, strings.ToLower(k), "token",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func runEvalList(ctx context.Context, flags *evalListFlags) error {
activeEvalID = state.EvalID
}

resp, err := resolved.evalClient.ListOpenAIEvals(ctx, flags.limit, DefaultAgentAPIVersion)
resp, err := resolved.evalClient.ListOpenAIEvals(ctx, flags.limit)
if err != nil {
return fmt.Errorf("failed to list evals: %w", err)
}
Expand All @@ -81,7 +81,7 @@ func runEvalList(ctx context.Context, flags *evalListFlags) error {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
runs, err := resolved.evalClient.ListOpenAIEvalRuns(ctx, evalID, 10, DefaultAgentAPIVersion)
runs, err := resolved.evalClient.ListOpenAIEvalRuns(ctx, evalID, 10)
if err != nil || runs == nil {
return
}
Expand Down
5 changes: 2 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/eval_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func runEvalRun(ctx context.Context, flags *evalRunFlags, noPrompt bool) error {

if evalID == "" {
created, err := resolved.evalClient.CreateOpenAIEval(
ctx, buildOpenAIEvalRequest(evalCfg), DefaultAgentAPIVersion,
ctx, buildOpenAIEvalRequest(evalCfg),
)
if err != nil {
return fmt.Errorf("failed to create eval: %w", err)
Expand Down Expand Up @@ -154,7 +154,6 @@ func runEvalRun(ctx context.Context, flags *evalRunFlags, noPrompt bool) error {
ctx,
evalID,
runReq,
DefaultAgentAPIVersion,
)
if err != nil {
return fmt.Errorf("failed to start eval run: %w", err)
Expand Down Expand Up @@ -255,7 +254,7 @@ func pollEvalRun(
case <-time.After(defaultEvalPollInterval):
}

run, err := client.GetOpenAIEvalRun(ctx, evalID, runID, DefaultAgentAPIVersion)
run, err := client.GetOpenAIEvalRun(ctx, evalID, runID)
if err != nil {
if agents.IsTransientError(err) {
consecutiveTransient++
Expand Down
Loading
Loading