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
2 changes: 2 additions & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,8 @@ Interactions 的 `google_search`、`google_maps`、`computer_use`、`mcp_server`

编码器将全部函数声明合并为一个 Tool entry;Google Search 与 Image Search 合并为一个 search entry 并分别占用 `searchTypes` 索引 `0/1`;Code Execution、URL Context 与 Maps 各占一个 entry。Google Maps 与 Code Execution/URL Context 构成互斥工具组,每个请求选择其中一组。Anthropic server tool 的定义规则见“Anthropic Messages”节。

Playground 的 `GenerateContent` 与 `CountTokens` 接受最长 63 字符的函数名。更长的名称在函数声明、历史函数调用与函数结果中统一编码为原名的前 50 个字符、`_` 与原名 SHA-256 前 6 字节的十六进制,共 63 字符;上游返回的函数调用在交给公开协议前换回声明的原名。写代码助手与 Live 会话按原名发送。

函数 JSON Struct 使用 protobuf `Struct/Value` 数组:map 为 `[[[key,value],...]]`;Value oneof 索引 `0..5` 分别表示 null、number、string、bool、Struct、ListValue。对象键排序后编码。

例如以下函数参数:
Expand Down
2 changes: 1 addition & 1 deletion internal/aistudio/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,7 @@ func (c *Client) sendPlayground(ctx context.Context, request GenerateRequest, en
return nil, nil, err
}
return response, func(source io.Reader, emit func(Event) error) error {
return c.decodePlaygroundRounds(ctx, request, entry.defaults, runtime, source, emit)
return c.decodePlaygroundRounds(ctx, request, entry.defaults, runtime, source, restoreFunctionNames(request.Tools.Functions, emit))
}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions internal/aistudio/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func encodePart(part Part) ([]any, error) {
if err != nil {
return nil, fmt.Errorf("function call arguments: %w", err)
}
call := []any{part.FunctionCall.Name, arguments}
call := []any{wireFunctionName(part.FunctionCall.Name), arguments}
if part.FunctionCall.ID != "" {
call = append(call, part.FunctionCall.ID)
}
Expand All @@ -388,7 +388,7 @@ func encodePart(part Part) ([]any, error) {
if err != nil {
return nil, fmt.Errorf("function result content: %w", err)
}
result := []any{part.FunctionResult.Name, response}
result := []any{wireFunctionName(part.FunctionResult.Name), response}
if part.FunctionResult.ID != "" {
result = append(result, part.FunctionResult.ID)
}
Expand Down
46 changes: 46 additions & 0 deletions internal/aistudio/tool_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,49 @@ func TestToolSchemaKeepsFileTools(t *testing.T) {
t.Fatal("原始工具集发生变化")
}
}

// TestToolSchemaLongFunctionNames 验证超过 63 字符的函数名以不超过 63 字符的同一名称声明与回传历史,上游调用以原名交给客户端
func TestToolSchemaLongFunctionNames(t *testing.T) {
prefix := "mcp__codex_apps__codex_document_control___execute_document_"
names := []string{prefix + "command", prefix + "commands", "Read"}
functions := make([]FunctionDeclaration, 0, len(names))
for _, name := range names {
functions = append(functions, FunctionDeclaration{Name: name, Parameters: json.RawMessage(`{"type":"object"}`)})
}
wire, _, err := encodeRequestedTools(Tools{Functions: functions})
if err != nil {
t.Fatal(err)
}
sent := make([]string, 0, len(names))
for _, declaration := range wire[0].([]any)[1].([]any) {
sent = append(sent, declaration.([]any)[0].(string))
}
if len(sent[0]) > 63 || len(sent[1]) > 63 || sent[0] == sent[1] || sent[2] != "Read" {
t.Fatalf("声明的函数名 %q", sent)
}
contents, err := encodeContents([]Content{
{Role: RoleUser, Parts: []Part{{Text: "run"}}},
{Role: RoleAssistant, Parts: []Part{{FunctionCall: &FunctionCall{ID: "call-1", Name: names[0], Arguments: json.RawMessage(`{}`)}}}},
{Role: RoleUser, Parts: []Part{{FunctionResult: &FunctionResult{ID: "call-1", Name: names[0], Content: json.RawMessage(`{"ok":true}`)}}}},
})
if err != nil {
t.Fatal(err)
}
history, _ := json.Marshal(contents)
if strings.Count(string(history), `"`+sent[0]+`"`) != 2 || strings.Contains(string(history), names[0]) {
t.Fatalf("历史中的函数名与声明不一致: %s", history)
}
var received []string
emit := restoreFunctionNames(functions, func(event Event) error {
received = append(received, event.FunctionCall.Name)
return nil
})
for _, name := range sent {
if err := emit(Event{Kind: EventToolCall, FunctionCall: &FunctionCall{Name: name}}); err != nil {
t.Fatal(err)
}
}
if !reflect.DeepEqual(received, names) {
t.Fatalf("回传的函数名 %q", received)
}
}
39 changes: 39 additions & 0 deletions internal/aistudio/tools.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package aistudio

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"math"
Expand All @@ -25,6 +27,7 @@ func encodeRequestedTools(tools Tools) ([]any, bool, error) {
if len(tools.Functions) > 0 {
declarations := make([]any, 0, len(tools.Functions))
for index, declaration := range tools.Functions {
declaration.Name = wireFunctionName(declaration.Name)
encoded, err := encodeFunctionDeclaration(declaration)
if err != nil {
return nil, false, fmt.Errorf("编码 function declaration %d: %w", index, err)
Expand Down Expand Up @@ -191,6 +194,42 @@ func functionBehavior(declaration FunctionDeclaration) (string, int64, error) {
return name, value, nil
}

// maxWireFunctionNameLength 是 Playground GenerateContent 与 CountTokens 接受的最长函数名
const maxWireFunctionNameLength = 63

// wireFunctionName 返回发往 Playground 的函数名,超长时截短并以原名的哈希结尾
func wireFunctionName(name string) string {
if len(name) <= maxWireFunctionNameLength {
return name
}
digest := sha256.Sum256([]byte(name))
suffix := "_" + hex.EncodeToString(digest[:6])
return name[:maxWireFunctionNameLength-len(suffix)] + suffix
}

// restoreFunctionNames 在 emit 前把函数调用中的 Playground 函数名换回声明的原名
func restoreFunctionNames(functions []FunctionDeclaration, emit func(Event) error) func(Event) error {
originals := map[string]string{}
for _, declaration := range functions {
if wire := wireFunctionName(declaration.Name); wire != declaration.Name {
originals[wire] = declaration.Name
}
}
if len(originals) == 0 {
return emit
}
return func(event Event) error {
if event.FunctionCall != nil {
if name, ok := originals[event.FunctionCall.Name]; ok {
call := *event.FunctionCall
call.Name = name
event.FunctionCall = &call
}
}
return emit(event)
}
}

func encodeFunctionDeclaration(declaration FunctionDeclaration) ([]any, error) {
if declaration.Name == "" {
return nil, fmt.Errorf("function declaration 缺少名称")
Expand Down
Loading