@@ -206,13 +206,60 @@ func (a *Adapter) Stream(ctx context.Context, req provider.ChatRequest) (<-chan
206206 if resp .StatusCode / 100 != 2 {
207207 raw , _ := io .ReadAll (io .LimitReader (resp .Body , 4096 ))
208208 _ = resp .Body .Close ()
209- return nil , fmt .Errorf ("%w: azure stream status %d: %s" , provider .ErrUpstream , resp .StatusCode , string (raw ))
209+ streamErr := fmt .Errorf ("%w: azure stream status %d: %s" , provider .ErrUpstream , resp .StatusCode , string (raw ))
210+ // Foundry OpenAI v1 (especially model-router) accepts the
211+ // same chat/completions URL for non-stream Chat that
212+ // Validate uses, but returns 404 DeploymentNotFound when
213+ // the body has stream:true. Retry as Chat and synthesize
214+ // chunks so Helix still completes.
215+ if resp .StatusCode == http .StatusNotFound {
216+ chatResp , chatErr := a .Chat (ctx , req )
217+ if chatErr == nil {
218+ return chatResponseAsStream (ctx , chatResp ), nil
219+ }
220+ }
221+ return nil , streamErr
210222 }
211223 out := make (chan provider.Chunk , 8 )
212224 go relayStream (ctx , resp .Body , out )
213225 return out , nil
214226}
215227
228+ // chatResponseAsStream turns a completed Chat response into the
229+ // Stream channel shape dispatch already consumes.
230+ func chatResponseAsStream (ctx context.Context , resp * provider.ChatResponse ) <- chan provider.Chunk {
231+ out := make (chan provider.Chunk , 8 )
232+ go func () {
233+ defer close (out )
234+ if resp == nil {
235+ send (ctx , out , provider.Chunk {Err : fmt .Errorf ("%w: azure stream fallback returned nil chat" , provider .ErrUpstream )})
236+ return
237+ }
238+ if resp .Message .Content != "" {
239+ send (ctx , out , provider.Chunk {Delta : resp .Message .Content })
240+ }
241+ for i := range resp .ToolCalls {
242+ call := resp .ToolCalls [i ]
243+ send (ctx , out , provider.Chunk {ToolDelta : & call })
244+ }
245+ finish := resp .FinishReason
246+ if finish == "" {
247+ if len (resp .ToolCalls ) > 0 {
248+ finish = provider .FinishToolCalls
249+ } else {
250+ finish = provider .FinishStop
251+ }
252+ }
253+ send (ctx , out , provider.Chunk {
254+ Done : true ,
255+ FinishReason : finish ,
256+ InputTokens : resp .InputTokens ,
257+ OutputTokens : resp .OutputTokens ,
258+ })
259+ }()
260+ return out
261+ }
262+
216263// Embed uses the Azure embeddings route. URL shape depends on flavor:
217264//
218265// - OpenAI flavor: {base}/openai/deployments/{depl}/embeddings?api-version=...
0 commit comments