v0.1.98 d55af26

azure_openai.go RawHistoryFolder

1package provider
2
3// Azure OpenAI client.
4//
5// Strategy: reuse the existing openaiClient (Chat Completions wire format)
6// but with three Azure-specific adjustments handled at construction time:
7//
8//   - URL shape: `{base}/openai/deployments/{model}/chat/completions?api-version={v}`
9//     This is wired by rewriting the model id into a path-prefix so the
10//     downstream code that appends `/v1/chat/completions` lands on the
11//     right endpoint. We achieve this by routing requests through a small
12//     RoundTripper that rewrites the outgoing URL before the request hits
13//     the network. The `openaiClient` itself is untouched.
14//   - Auth: `api-key: <key>` header instead of `Authorization: Bearer ...`.
15//     Sent via openaiClient.headers; the standard Authorization header
16//     remains but Azure ignores it.
17//   - API version: appended as `?api-version=...` query string by the
18//     same RoundTripper.
19//
20// We deliberately use Chat Completions (not the newer Responses API) so we
21// don't have to duplicate the entire openai-responses wire client. The
22// agent loop only needs streamed text + tool calls, both of which work
23// fine on Azure's Chat Completions deployment.
24
25import (
26	"fmt"
27	"net/http"
28	"net/url"
29	"os"
30	"strings"
31)
32
33const defaultAzureAPIVersion = "2024-10-21"
34
35// azureRewriteTransport rewrites every outgoing OpenAI Chat Completions
36// URL into the Azure deployment-scoped shape:
37//
38//	/openai/v1/chat/completions  ->
39//	/openai/deployments/{model}/chat/completions?api-version={v}
40//
41// The model id is read from the JSON body of the POST so we don't need to
42// thread it through the client at construction time. This keeps the
43// openaiClient request-builder untouched.
44type azureRewriteTransport struct {
45	inner      http.RoundTripper
46	apiVersion string
47}
48
49func (t *azureRewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
50	if strings.Contains(req.URL.Path, "/chat/completions") {
51		// Extract model id from the JSON body. The body is a *bytes.Reader
52		// (set up by openaiClient.Stream) so we can peek without consuming
53		// the request — Go's http.NewRequestWithContext gives us
54		// GetBody() automatically for *bytes.Reader inputs.
55		modelID := ""
56		if req.GetBody != nil {
57			if body, err := req.GetBody(); err == nil {
58				buf := make([]byte, 8192)
59				n, _ := body.Read(buf)
60				_ = body.Close()
61				// Cheap manual scan: look for `"model":"<id>"`.
62				s := string(buf[:n])
63				if i := strings.Index(s, `"model":"`); i >= 0 {
64					rest := s[i+len(`"model":"`):]
65					if j := strings.Index(rest, `"`); j >= 0 {
66						modelID = rest[:j]
67					}
68				}
69			}
70		}
71		if modelID == "" {
72			return nil, fmt.Errorf("azure: could not extract model id from request body")
73		}
74		// Strip whatever prefix the openaiClient picked and replace with
75		// Azure's deployment path. Both `/openai/v1/chat/completions` and
76		// plain `/chat/completions` are normalised.
77		newPath := "/openai/deployments/" + url.PathEscape(modelID) + "/chat/completions"
78		// Anchor at the host root; the openaiClient may have used a
79		// base URL with no path or with `/openai/v1`.
80		req.URL.Path = newPath
81		q := req.URL.Query()
82		q.Set("api-version", t.apiVersion)
83		req.URL.RawQuery = q.Encode()
84	}
85	return t.inner.RoundTrip(req)
86}
87
88// NewAzureOpenAI returns an Azure OpenAI client.
89//
90// baseURL examples (any is fine; we normalise trailing slashes):
91//
92//	https://my-resource.openai.azure.com
93//	https://my-resource.openai.azure.com/openai/v1
94//
95// apiKey is the Azure resource key. The api-version comes from
96// AZURE_OPENAI_API_VERSION env var (default "2024-10-21").
97func NewAzureOpenAI(apiKey, baseURL string) Client {
98	if baseURL == "" {
99		// Try env, then resource-name expansion.
100		baseURL = os.Getenv("AZURE_OPENAI_BASE_URL")
101		if baseURL == "" {
102			if rn := os.Getenv("AZURE_OPENAI_RESOURCE_NAME"); rn != "" {
103				baseURL = "https://" + rn + ".openai.azure.com"
104			}
105		}
106	}
107	if baseURL == "" {
108		return &unimplementedClient{
109			name: "azure-openai-responses",
110			hint: "set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME (or pass --base-url)",
111		}
112	}
113	apiVersion := os.Getenv("AZURE_OPENAI_API_VERSION")
114	if apiVersion == "" {
115		apiVersion = defaultAzureAPIVersion
116	}
117	// Strip a trailing /openai/v1 (and /openai) since the rewrite transport
118	// builds the full path itself.
119	clean := strings.TrimRight(baseURL, "/")
120	clean = strings.TrimSuffix(clean, "/openai/v1")
121	clean = strings.TrimSuffix(clean, "/openai")
122	httpClient := &http.Client{
123		Transport: &azureRewriteTransport{
124			inner:      http.DefaultTransport,
125			apiVersion: apiVersion,
126		},
127		Timeout: 0,
128	}
129	return &openaiClient{
130		apiKey:  apiKey,
131		baseURL: clean,
132		name:    "azure-openai-responses",
133		headers: map[string]string{"api-key": apiKey},
134		http:    httpClient,
135	}
136}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close