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
6 changes: 3 additions & 3 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ ListModels 能力码 91(目录能力 `output_continuation`)的模型由官
| 18 | missing_thought_signature | 19 | malformed_response |
| 22 | continuation | 其他整数 | `provider_<code>` |

Build 通道的 Gemini API 终止原因去掉 `FINISH_REASON_` 前缀后转为上表的同名小写原因,上表没有的名称为 `provider_<小写名称>`。上游以 `malformed_function_call` 或 `unexpected_tool_call` 结束,或流在完成帧之前中断、以 5xx 流尾状态结束,且这次生成没有输出正文、函数调用与媒体时,服务在同一账户上以同一请求重新生成,最多再生成 3 次,之后按最后一次的终止原因或中断错误结束;每次重新生成从头匹配停止序列,上一次暂存的停止序列前缀不输出,重新生成的请求发送失败时本次生成以该错误结束。两个通道第一次带终止原因的帧中的内容事件标记为结束帧,自动续写中转入下一轮的帧不标记;Gemini 流式输出把结束帧的内容放进最后一块。
Build 通道的 Gemini API 终止原因去掉 `FINISH_REASON_` 前缀后转为上表的同名小写原因,上表没有的名称为 `provider_<小写名称>`。上游以 `malformed_function_call`、`unexpected_tool_call` 或 `malformed_response` 结束,或流在完成帧之前中断、以 5xx 流尾状态结束,且这次生成没有输出正文、函数调用与媒体时(不输出给客户端的搜索、网址上下文、地图与文件检索步骤不计入,MCP 调用计入),服务在同一账户上以同一请求重新生成,最多再生成 3 次,之后按最后一次的终止原因或中断错误结束;被重新生成取代的那一次的网页来源与引用不输出,每次重新生成从头匹配停止序列,上一次暂存的停止序列前缀不输出,重新生成的请求发送失败时本次生成以该错误结束。两个通道第一次带终止原因的帧中的内容事件标记为结束帧,自动续写中转入下一轮的帧不标记;Gemini 流式输出把结束帧的内容放进最后一块。

候选 field 5(`$[0][frame][0][0][4]`)为安全评级 `[[null, null, 类别, 概率, blocked?], ...]`,类别编号与安全设置相同,概率 `NEGLIGIBLE`=1、`LOW`=2、`MEDIUM`=3、`HIGH`=4;输入拒绝帧 `[null,[block_reason,ratings?,message]]` 的 ratings 为同形数组。Build 通道读取候选与 `promptFeedback` 的 `safetyRatings`。

Expand Down Expand Up @@ -782,8 +782,8 @@ generation config、安全设置、工具与转写配置在选择账户前按各
| `stop_sequence` | `stop` | `completed` | `stop_sequence` 与实际序列 | `STOP` |
| `max_tokens` | `length` | `incomplete/max_output_tokens` | `max_tokens` | `MAX_TOKENS` |
| `continuation` | `length` | `incomplete/max_output_tokens` | `max_tokens` | `CONTINUATION` |
| safety、recitation、blocklist、prohibited_content、spii、malformed_response、图片对应原因与 `provider_*` | `content_filter` | `incomplete/content_filter` | `refusal` | 对应 Gemini 枚举或 `OTHER` |
| other、language、malformed_function_call、签名缺失等其他终态 | `stop`,含函数调用时 `tool_calls` | `completed` | `end_turn`,含函数调用时 `tool_use` | 对应 Gemini 枚举 |
| safety、recitation、blocklist、prohibited_content、spii、图片对应原因与 `provider_*` | `content_filter` | `incomplete/content_filter` | `refusal` | 对应 Gemini 枚举或 `OTHER` |
| other、language、malformed_function_call、malformed_response、签名缺失等其他终态 | `stop`,含函数调用时 `tool_calls` | `completed` | `end_turn`,含函数调用时 `tool_use` | 对应 Gemini 枚举 |

内容拦截终态优先于同一结果中的工具调用终态,已产生的正文、reasoning、工具事件和 usage 保持在响应中。`provider_*` 在 Chat choice、Responses response、Anthropic message 或 `message_delta` 的 `provider_finish_reason` 中保留原值;Gemini 对 Build 通道的 `provider_<名称>` 输出大写的原枚举名,对 Playground 的 `provider_<编号>` 输出 `OTHER` 并用 `finishMessage` 保留编号。`malformed_response`(Playground 编号 19)在 AI Studio 页面显示为 `Content blocked`。

Expand Down
46 changes: 37 additions & 9 deletions internal/aistudio/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var errStopSequenceMatched = errors.New("stop sequence matched")
// continuationRoundTokens 为官网自动续写每轮增加的输出上限
const continuationRoundTokens = 32768

// regenerations 是上游以函数调用格式错误结束、流中断或流尾 5xx 错误且客户端没有收到内容时重新生成的次数上限
// regenerations 是上游以函数调用格式错误、意外的工具调用或响应格式异常结束、流中断或流尾 5xx 错误且客户端没有收到内容时重新生成的次数上限
const regenerations = 3

// tokenCountResult 保存并发输入计数结果
Expand Down Expand Up @@ -861,6 +861,19 @@ func (c *Client) Generate(ctx context.Context, request GenerateRequest) (<-chan
var usage *Usage
var finish *Event
var output Output
// committed 表示本次生成已有不能随重新生成丢弃的输出:客户端收到的正文、函数调用与媒体,或重新执行有外部副作用的服务端工具调用
committed := false
// sources 暂存 committed 之前收到的网页来源与引用,重新生成时丢弃
var sources []Event
releaseSources := func() error {
for _, source := range sources {
if err := send(source); err != nil {
return err
}
}
sources = nil
return nil
}
matchedStopSequence := ""
emitEvent := func(event Event) error {
switch event.Kind {
Expand All @@ -873,8 +886,20 @@ func (c *Client) Generate(ctx context.Context, request GenerateRequest) (<-chan
finish = &value
return nil
default:
output.Add(event)
if request.Config.HideThinking && event.Kind == EventReasoning || serverToolHidden(request, event) {
hiddenTool := serverToolHidden(request, event)
if !hiddenTool || !serverToolReplayable(event) {
output.Add(event)
committed = committed || !event.Thought && event.payload()
}
if !committed {
if event.Kind == EventGrounding || event.Kind == EventCitation {
sources = append(sources, event)
return nil
}
} else if err := releaseSources(); err != nil {
return err
}
if request.Config.HideThinking && event.Kind == EventReasoning || hiddenTool {
if event.ThoughtSignature == "" {
return nil
}
Expand All @@ -888,7 +913,7 @@ func (c *Client) Generate(ctx context.Context, request GenerateRequest) (<-chan
}
emit := stopSequenceEmitter(matcher, emitEvent, &matchedStopSequence)
err := decodeStream(observeStreamActivity(ctx, response.Body), emit)
for regenerated := 0; regenerated < regenerations && regenerable(ctx, err, finish, output); regenerated++ {
for regenerated := 0; regenerated < regenerations && regenerable(ctx, err, finish, committed); regenerated++ {
stopClose()
_ = response.Body.Close()
if response, decodeStream, err = sendRequest(); err != nil {
Expand All @@ -898,11 +923,14 @@ func (c *Client) Generate(ctx context.Context, request GenerateRequest) (<-chan
return
}
stopClose = closeOnCancel(response)
usage, finish = nil, nil
usage, finish, sources = nil, nil, nil
matcher = newStopSequenceMatcher(request.Config.StopSequences)
emit = stopSequenceEmitter(matcher, emitEvent, &matchedStopSequence)
err = decodeStream(observeStreamActivity(ctx, response.Body), emit)
}
if releaseSources() != nil {
return
}
if errors.Is(err, errStopSequenceMatched) {
_ = response.Body.Close()
finishAtStopSequence(ctx, request, output, stopTokenCount, matchedStopSequence, send)
Expand Down Expand Up @@ -944,18 +972,18 @@ func (c *Client) Generate(ctx context.Context, request GenerateRequest) (<-chan
return contract.forward(ctx, events), nil
}

// regenerable 表示上游以函数调用格式错误或意外的工具调用结束、流在完成帧之前中断或流尾为 5xx 错误,且没有输出正文、函数调用与媒体
func regenerable(ctx context.Context, err error, finish *Event, output Output) bool {
// regenerable 表示上游以函数调用格式错误、意外的工具调用或响应格式异常结束、流在完成帧之前中断或流尾为 5xx 错误,且本次生成还没有不能丢弃的输出
func regenerable(ctx context.Context, err error, finish *Event, committed bool) bool {
if err != nil {
var interrupted *StreamInterruptedError
var rpcError *RPCError
if ctx.Err() != nil || !errors.As(err, &interrupted) && !(errors.As(err, &rpcError) && rpcError.StatusCode >= http.StatusInternalServerError) {
return false
}
} else if finish == nil || finish.FinishReason != "malformed_function_call" && finish.FinishReason != "unexpected_tool_call" {
} else if finish == nil || finish.FinishReason != "malformed_function_call" && finish.FinishReason != "unexpected_tool_call" && finish.FinishReason != "malformed_response" {
return false
}
return !slices.ContainsFunc(output.Visible(), Part.payload)
return !committed
}

// countStopInput 并发计数请求输入,供本地命中 stop sequence 后补全用量
Expand Down
6 changes: 6 additions & 0 deletions internal/aistudio/server_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aistudio
import (
"encoding/json"
"fmt"
"slices"
)

// serverToolTypes 是 Playground ToolCall 与 ToolResponse field 1 的工具类型编号,MCP 调用为 CUSTOM,MCP 结果不带类型
Expand All @@ -26,6 +27,11 @@ func serverToolHidden(request GenerateRequest, event Event) bool {
return step != nil && step.Type != "MEDIA_PROCESSING" && !request.Tools.ToolConfig.ServerSideInvocations
}

// serverToolReplayable 表示事件随重新生成在上游再次执行没有外部副作用:网页与图片搜索、网址上下文、地图与文件检索的调用和全部工具结果,MCP 调用与其他类型的调用除外
func serverToolReplayable(event Event) bool {
return event.ToolCall == nil || slices.Contains([]string{"GOOGLE_SEARCH_WEB", "GOOGLE_SEARCH_IMAGE", "URL_CONTEXT", "GOOGLE_MAPS", "FILE_SEARCH"}, event.ToolCall.Type)
}

// encodeServerToolStep 编码 ToolCall 或 ToolResponse:工具类型、名称、Struct 与调用 ID
func encodeServerToolStep(step *ServerToolStep) ([]any, error) {
stepType := step.Type
Expand Down
2 changes: 1 addition & 1 deletion internal/api/openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,7 @@ func legacyFunctionCall(call aistudio.FunctionCall) map[string]any {
func contentFilterFinishReason(reason string) bool {
switch reason {
case "safety", "recitation", "blocklist", "prohibited_content", "spii",
"image_safety", "image_prohibited_content", "image_recitation", "malformed_response":
"image_safety", "image_prohibited_content", "image_recitation":
return true
}
return strings.HasPrefix(reason, "provider_")
Expand Down