fix(codex,memory): sanitize tool call IDs, fix memory delete, add GPT-5.4 (#313)

* fix(codex,memory): sanitize tool call IDs, fix memory delete, add GPT-5.4

- codex_build: sanitize colons in tool call IDs before sending to
  Responses API (OpenAI rejects non-alphanumeric chars except _ and -)
- memory_docs: check RowsAffected on delete — return error instead of
  silent 200 when no document matched
- memory UI: pass document's agent_id to delete call so deletion works
  from the "all agents" view (previously sent undefined as agent ID)
- codex models: add GPT-5.4 and GPT-5.4 Mini, update default model

* fix(codex): update default model assertion in test to gpt-5.4

* fix(codex): regex sanitize all invalid chars in tool call IDs, restore GPT-5.1 models

- toFcID: replace all non-alphanumeric/underscore/dash chars (not just
  colons) to prevent future Responses API rejections
- Restore GPT-5.1 Codex Max and Codex Mini in model dropdown to avoid
  breaking existing user configurations

---------

Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
Co-authored-by: viettranx <viettranx@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-03-20 21:57:30 +07:00
1 parent e4770956cb
commit c6bab9a059
7 files changed
+41 -20

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ func NewCodexProvider(name string, tokenSource TokenSource, apiBase, defaultMode
apiBase = strings.TrimRight(apiBase, "/")
if defaultModel == "" {
defaultModel = "gpt-5.3-codex"
defaultModel = "gpt-5.4"
}
return &CodexProvider{
+13 -8
View File
@@ -7,9 +7,13 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
)
// invalidFcIDChars matches characters not allowed in Responses API tool call IDs.
var invalidFcIDChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
// buildRequestBody converts internal ChatRequest to Responses API format.
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]any {
model := req.Model
@@ -164,16 +168,17 @@ func (p *CodexProvider) doRequest(ctx context.Context, body any) (io.ReadCloser,
return resp.Body, nil
}
// toFcID ensures a tool call ID starts with "fc_" as required by the Responses API.
// toFcID ensures a tool call ID starts with "fc_" and contains only
// letters, numbers, underscores, or dashes as required by the Responses API.
func toFcID(id string) string {
if strings.HasPrefix(id, "fc_") {
return id
}
if strings.HasPrefix(id, "tool_") {
return "fc_" + id[len("tool_"):]
}
if strings.HasPrefix(id, "call_") {
return "fc_" + id[len("call_"):]
id = id[len("tool_"):]
} else if strings.HasPrefix(id, "call_") {
id = id[len("call_"):]
} else if strings.HasPrefix(id, "fc_") {
id = id[len("fc_"):]
}
// Replace invalid characters (e.g. colons from session keys) with underscores.
id = invalidFcIDChars.ReplaceAllString(id, "_")
return "fc_" + id
}
+2 -2
View File
@@ -47,8 +47,8 @@ func TestCodexProviderName(t *testing.T) {
func TestCodexProviderDefaultModel(t *testing.T) {
p := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "")
if p.DefaultModel() != "gpt-5.3-codex" {
t.Errorf("DefaultModel() = %q, want %q", p.DefaultModel(), "gpt-5.3-codex")
if p.DefaultModel() != "gpt-5.4" {
t.Errorf("DefaultModel() = %q, want %q", p.DefaultModel(), "gpt-5.4")
}
p2 := NewCodexProvider("test", &staticTokenSource{token: "test"}, "", "o3")
+14 -5
View File
@@ -85,16 +85,25 @@ func (s *PGMemoryStore) PutDocument(ctx context.Context, agentID, userID, path,
func (s *PGMemoryStore) DeleteDocument(ctx context.Context, agentID, userID, path string) error {
aid := mustParseUUID(agentID)
var res sql.Result
var err error
if userID == "" {
_, err := s.db.ExecContext(ctx,
res, err = s.db.ExecContext(ctx,
"DELETE FROM memory_documents WHERE agent_id = $1 AND path = $2 AND user_id IS NULL",
aid, path)
} else {
res, err = s.db.ExecContext(ctx,
"DELETE FROM memory_documents WHERE agent_id = $1 AND path = $2 AND user_id = $3",
aid, path, userID)
}
if err != nil {
return err
}
_, err := s.db.ExecContext(ctx,
"DELETE FROM memory_documents WHERE agent_id = $1 AND path = $2 AND user_id = $3",
aid, path, userID)
return err
n, _ := res.RowsAffected()
if n == 0 {
return fmt.Errorf("document not found: %s", path)
}
return nil
}
func (s *PGMemoryStore) ListDocuments(ctx context.Context, agentID, userID string) ([]store.DocumentInfo, error) {
+7 -2
View File
@@ -95,10 +95,15 @@ export function useMemoryDocuments(filters: MemoryDocFilters) {
);
const deleteDocument = useCallback(
async (path: string, userId?: string) => {
async (path: string, userId?: string, agentId?: string) => {
const aid = agentId || filters.agentId;
if (!aid) {
toast.error(i18n.t("memory:toast.docDeleteFailed"), "No agent selected");
return;
}
try {
const qs = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
await http.delete(`/v1/agents/${filters.agentId}/memory/documents/${path}${qs}`);
await http.delete(`/v1/agents/${aid}/memory/documents/${path}${qs}`);
await invalidate();
toast.success(i18n.t("memory:toast.docDeleted"), path);
} catch (err) {
+1 -1
View File
@@ -82,7 +82,7 @@ export function MemoryPage() {
if (!deleteTarget) return;
setDeleteLoading(true);
try {
await deleteDocument(deleteTarget.path, deleteTarget.user_id);
await deleteDocument(deleteTarget.path, deleteTarget.user_id, deleteTarget.agent_id);
setDeleteTarget(null);
} finally {
setDeleteLoading(false);
@@ -8,13 +8,15 @@ export type { ModelInfo };
// Hardcoded models for ChatGPT OAuth provider.
// OAuth token lacks api.model.read scope, so we can't call /v1/models.
const CODEX_MODELS: ModelInfo[] = [
{ id: "gpt-5.4", name: "GPT-5.4" },
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
{ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
{ id: "gpt-5.2", name: "GPT-5.2" },
{ id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini" },
{ id: "gpt-5.1-codex", name: "GPT-5.1 Codex" },
{ id: "gpt-5.1", name: "GPT-5.1" },
];