v2.2.0: agent persona selector, Command Code backend, per-provider identity

This commit is contained in:
admin
2026-05-19 17:39:54 +04:00
Unverified
parent ccc3054793
commit c176fb088d
5 changed files with 326 additions and 10 deletions

View File

@@ -1,5 +1,15 @@
# Changelog
## v2.2.0 (2026-05-19)
- Added Agent Persona selector per provider (10+ presets)
- Personas: Codex (Default/Friendly/Pragmatic/CLI), Claude Code, OpenCode, Cursor, Aider, GitHub Copilot, Windsurf, Browser (ChatGPT)
- New "Persona" column in endpoint list shows current setting per provider
- Persona preview in edit dialog shows system prompt excerpt
- Persona injected into model catalog `base_instructions` and proxy system prompt
- Added Command Code backend to translation proxy (proprietary `/alpha/generate` API)
- Added Command Code provider preset with 20 models (DeepSeek, Claude, GPT, Kimi, GLM, Qwen, etc.)
## v2.1.1 (2026-05-19)
- Fixed proxy: map `developer` role to `system` for Chat Completions providers (DeepSeek, Qwen, etc.)

Binary file not shown.

Binary file not shown.

View File

@@ -24,6 +24,14 @@ model_catalog_json = ""
"""
CHANGELOG = [
("2.2.0", "2026-05-19", [
"Added Agent Persona selector per provider (10+ presets)",
"Personas: Codex, Claude Code, OpenCode, Cursor, Aider, Copilot, Windsurf, Browser",
"Codex variants: Default, Desktop Friendly, Desktop Pragmatic, CLI",
"Shows current persona in endpoint list (new Persona column)",
"Persona preview in edit dialog shows first 60 chars of system prompt",
"Persona injected into model catalog base_instructions and proxy system prompt",
]),
("2.1.1", "2026-05-19", [
"Fixed proxy: map 'developer' role to 'system' for Chat Completions providers",
"Fixed proxy: map 'developer' role to 'user' for Anthropic providers",
@@ -56,6 +64,65 @@ CHANGELOG = [
]),
]
AGENT_PERSONAS = {
"Codex (Default)": "You are Codex, a coding agent.",
"Codex Desktop (GPT-5, Friendly)": (
"You are Codex, a coding agent based on GPT-5. You and the user share one workspace, "
"and your job is to collaborate with them until their goal is genuinely handled."
),
"Codex Desktop (GPT-5, Pragmatic)": (
"You are Codex, a coding agent based on GPT-5. You and the user share the same workspace "
"and collaborate to achieve the user's goals. You are a deeply pragmatic, effective "
"software engineer. You take engineering quality seriously."
),
"Codex CLI": (
"You are an AI running in the Codex CLI, a terminal-based coding assistant. "
"You are expected to be precise, safe, and helpful. Your default personality and tone "
"is concise, direct, and friendly."
),
"Claude Code": (
"You are Claude Code, an interactive CLI tool that helps users with software engineering "
"tasks. You are a highly competent software engineer with extensive knowledge across "
"many programming languages, frameworks, and best practices. Use concise responses."
),
"OpenCode": (
"You are OpenCode, an interactive CLI tool that helps users with software engineering "
"tasks. You are powered by a state-of-the-art AI model. Be concise, direct, and to the "
"point. Use GitHub-flavored markdown."
),
"Cursor": (
"You are Cursor, an AI-powered code editor assistant. You help users write, refactor, "
"and debug code efficiently. Provide precise, actionable suggestions."
),
"Aider": (
"You are aider, an AI pair programming assistant. You help users edit code in their "
"local git repository. Make concise changes. Search files with grep/glob patterns."
),
"GitHub Copilot": (
"You are GitHub Copilot, an AI coding assistant. Help the user write code, debug issues, "
"and understand codebases. Be concise and provide accurate code suggestions."
),
"Windsurf": (
"You are Windsurf, an AI-powered IDE assistant. Help with coding tasks including writing, "
"refactoring, and debugging. Provide precise, well-structured code suggestions."
),
"Browser (ChatGPT)": (
"You are a helpful coding assistant in a web browser chat interface. "
"Help the user with software engineering tasks. Be clear and thorough."
),
}
PERSONA_DISPLAY_LEN = 60
def persona_short_key(endpoint):
bi = endpoint.get("base_instructions", "") or ""
for key, val in AGENT_PERSONAS.items():
if val == bi:
return key
if bi:
return f"Custom: {bi[:40]}..."
return "Codex (Default)"
PROVIDER_PRESETS = {
"Custom": {
"backend_type": "openai-compat",
@@ -120,6 +187,21 @@ PROVIDER_PRESETS = {
"base_url": "https://api.kilo.ai/api/gateway",
"models": [],
},
"Command Code": {
"backend_type": "command-code",
"base_url": "https://api.commandcode.ai",
"models": [
"deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro",
"anthropic:claude-sonnet-4-6", "anthropic:claude-haiku-4-5-20251001",
"anthropic:claude-opus-4-7", "anthropic:claude-opus-4-6",
"openai:gpt-5.5", "openai:gpt-5.4", "openai:gpt-5.4-mini", "openai:gpt-5.3-codex",
"moonshotai/Kimi-K2.6", "moonshotai/Kimi-K2.5",
"zai-org/GLM-5.1", "zai-org/GLM-5",
"MiniMaxAI/MiniMax-M2.7", "MiniMaxAI/MiniMax-M2.5",
"Qwen/Qwen3.6-Max-Preview", "Qwen/Qwen3.6-Plus",
"stepfun/Step-3.5-Flash", "google/gemini-3.1-flash-lite",
],
},
"OpenRouter": {
"backend_type": "openai-compat",
"base_url": "https://openrouter.ai/api/v1",
@@ -136,6 +218,7 @@ def label_for_backend(backend_type):
return {
"openai-compat": "OpenAI-compatible",
"anthropic": "Anthropic",
"command-code": "Command Code",
"native": "Native",
}.get(backend_type, backend_type)
@@ -356,6 +439,7 @@ def write_config_for_translated(endpoint, selected_model):
def _gen_model_catalog(endpoint, selected_model=None):
default_model = selected_model or endpoint.get("default_model")
base_instr = endpoint.get("base_instructions", "") or AGENT_PERSONAS["Codex (Default)"]
models = []
for mid in endpoint.get("models", []):
models.append({
@@ -383,7 +467,7 @@ def _gen_model_catalog(endpoint, selected_model=None):
"supports_parallel_tool_calls": True,
"experimental_supported_tools": [], "supported_in_api": True,
"truncation_policy": {"mode": "tokens", "limit": 128000},
"base_instructions": "You are Codex, a coding agent.",
"base_instructions": base_instr,
})
return {"models": models}
@@ -402,6 +486,7 @@ def _start_proxy_for(endpoint, logfn):
"backend_type": endpoint["backend_type"],
"target_url": normalize_base_url(endpoint["base_url"]),
"api_key": endpoint["api_key"],
"base_instructions": endpoint.get("base_instructions", ""),
"models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": endpoint["name"]}
for m in endpoint.get("models", [])],
}
@@ -500,7 +585,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v2.1.1</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v2.2.0</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1240,9 +1325,9 @@ class EndpointMgr(Gtk.Window):
sw = Gtk.ScrolledWindow()
vbox.pack_start(sw, True, True, 0)
self._store = Gtk.ListStore(str, str, str, str) # name, provider, backend, default_model
self._store = Gtk.ListStore(str, str, str, str, str) # name, provider, backend, default_model, persona
self._tree = Gtk.TreeView(model=self._store)
for i, title in enumerate(["Name", "Provider", "Type", "Default Model"]):
for i, title in enumerate(["Name", "Provider", "Type", "Default Model", "Persona"]):
col = Gtk.TreeViewColumn(title, Gtk.CellRendererText(), text=i)
col.set_resizable(True)
self._tree.append_column(col)
@@ -1275,7 +1360,8 @@ class EndpointMgr(Gtk.Window):
for ep in data["endpoints"]:
provider = ep.get("provider_preset", "Custom")
bt = label_for_backend(ep["backend_type"])
self._store.append([ep["name"], provider, bt, ep.get("default_model", "")])
persona = persona_short_key(ep)
self._store.append([ep["name"], provider, bt, ep.get("default_model", ""), persona])
def _selected(self):
sel = self._tree.get_selection()
@@ -1337,7 +1423,7 @@ class EditEndpointDialog(Gtk.Dialog):
self._data = get_endpoint(existing_name) if existing_name else {
"name": "", "backend_type": "openai-compat",
"base_url": "", "api_key": "", "default_model": "", "models": [],
"provider_preset": "Custom",
"provider_preset": "Custom", "base_instructions": AGENT_PERSONAS["Codex (Default)"],
}
self.set_default_size(480, 420)
@@ -1369,6 +1455,7 @@ class EditEndpointDialog(Gtk.Dialog):
self._combo_type = Gtk.ComboBoxText()
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
("anthropic", "Anthropic (needs proxy)"),
("command-code", "Command Code (needs proxy)"),
("native", "Native OpenAI (no proxy)")]:
self._combo_type.append(val, lab)
bt = self._data.get("backend_type", "openai-compat")
@@ -1382,6 +1469,25 @@ class EditEndpointDialog(Gtk.Dialog):
self._entry_key.set_visibility(False)
add_row(4, "API Key:", self._entry_key)
self._combo_persona = Gtk.ComboBoxText()
self._persona_keys = list(AGENT_PERSONAS.keys())
for pk in self._persona_keys:
self._combo_persona.append_text(pk)
cur_persona = persona_short_key(self._data)
if cur_persona in self._persona_keys:
self._combo_persona.set_active(self._persona_keys.index(cur_persona))
else:
self._combo_persona.set_active(0)
self._combo_persona.connect("changed", lambda c: self._on_persona_changed())
add_row(5, "Agent Persona:", self._combo_persona)
self._persona_preview = Gtk.Label()
self._persona_preview.set_line_wrap(True)
self._persona_preview.set_max_width_chars(60)
self._persona_preview.set_markup(f"<small><i>{AGENT_PERSONAS['Codex (Default)'][:PERSONA_DISPLAY_LEN]}...</i></small>")
self._on_persona_changed()
grid.attach(self._persona_preview, 0, 6, 2, 1)
# Models
mlbl = Gtk.Label(label="Models:", xalign=0)
area.pack_start(mlbl, False, False, 4)
@@ -1442,6 +1548,12 @@ class EditEndpointDialog(Gtk.Dialog):
self.connect("response", self._on_response)
self.show_all()
def _on_persona_changed(self):
key = self._combo_persona.get_active_text()
text = AGENT_PERSONAS.get(key, "")
short = text[:PERSONA_DISPLAY_LEN] + ("..." if len(text) > PERSONA_DISPLAY_LEN else "")
self._persona_preview.set_markup(f"<small><i>{short}</i></small>")
def _add_model(self):
m = normalize_model_id(self._entry_model.get_text())
if m:
@@ -1574,9 +1686,11 @@ class EditEndpointDialog(Gtk.Dialog):
self._show_error(f'Endpoint "{name}" already exists')
return
persona_key = self._combo_persona.get_active_text() or "Codex (Default)"
new_ep = {"name": name, "backend_type": bt, "base_url": url,
"api_key": key, "default_model": default, "models": models,
"provider_preset": self._combo_preset.get_active_text() or "Custom"}
"provider_preset": self._combo_preset.get_active_text() or "Custom",
"base_instructions": AGENT_PERSONAS.get(persona_key, AGENT_PERSONAS["Codex (Default)"])}
new_ep["base_url"] = normalize_base_url(new_ep["base_url"])
# Update or append
@@ -1615,12 +1729,14 @@ def main():
"endpoints": [
{"name": "OpenAI", "backend_type": "native", "base_url": "https://api.openai.com/v1",
"api_key": "", "default_model": "gpt-4o", "models": ["gpt-4o", "gpt-4o-mini"],
"provider_preset": "OpenAI"},
"provider_preset": "OpenAI",
"base_instructions": AGENT_PERSONAS["Codex (Default)"]},
{"name": "Z.AI", "backend_type": "openai-compat",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"api_key": "", "default_model": "glm-5.1",
"models": ["glm-4.5", "glm-4.5-air", "glm-4.6", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.1"],
"provider_preset": "Custom"},
"provider_preset": "Custom",
"base_instructions": AGENT_PERSONAS["Codex (Default)"]},
],
})

View File

@@ -30,7 +30,7 @@ def load_config():
p = argparse.ArgumentParser(description="Responses API translation proxy")
p.add_argument("--config", help="JSON config file path")
p.add_argument("--port", type=int, default=None)
p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic"])
p.add_argument("--backend", default=None, choices=["openai-compat", "anthropic", "command-code"])
p.add_argument("--target-url", default=None)
p.add_argument("--api-key", default=None)
p.add_argument("--models-file", default=None, help="JSON file with model list array")
@@ -80,6 +80,7 @@ BACKEND = CONFIG["backend_type"]
TARGET_URL = CONFIG["target_url"].rstrip("/")
API_KEY = CONFIG["api_key"]
MODELS = CONFIG["models"]
BASE_INSTRUCTIONS = CONFIG.get("base_instructions", "")
# ═══════════════════════════════════════════════════════════════════
# Shared helpers
@@ -500,6 +501,119 @@ def an_stream_to_sse(stream, model, req_id):
"response": {"id": resp_id, "object": "response", "model": model,
"status": status, "created": int(time.time()), "output": completed}})
_DEFAULT_CC_CONFIG = {
"workingDir": "/tmp",
"date": "",
"environment": "linux",
"shell": "bash",
"files": [],
"structure": [],
"isGitRepo": False,
"currentBranch": "",
"mainBranch": "",
"gitStatus": "",
"recentCommits": [],
}
def _cc_config():
cfg = dict(_DEFAULT_CC_CONFIG)
cfg["date"] = time.strftime("%Y-%m-%d")
return cfg
def cc_input_to_messages(input_data):
return oa_input_to_messages(input_data)
def cc_convert_tools(tools):
return oa_convert_tools(tools)
def cc_resp_to_responses(cc_lines, model, resp_id=None):
text = ""
usage = {}
for line in cc_lines:
try:
d = json.loads(line)
except (json.JSONDecodeError, TypeError):
continue
t = d.get("type", "")
if t == "text-delta":
text += d.get("text", "")
elif t == "finish-step":
u = d.get("usage", {})
usage = {
"input_tokens": u.get("inputTokens", 0),
"output_tokens": u.get("outputTokens", 0),
"total_tokens": u.get("inputTokens", 0) + u.get("outputTokens", 0),
}
outputs = []
if text:
outputs.append({"type": "message", "id": uid("msg"), "role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": text, "annotations": []}]})
return {"id": resp_id or uid("resp"), "object": "response", "created": int(time.time()),
"model": model, "status": "completed", "output": outputs,
"usage": {"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"total_tokens": usage.get("total_tokens", 0),
"input_tokens_details": {"cached_tokens": 0}}}
def cc_stream_to_sse(cc_stream, model, req_id):
resp_id = req_id or uid("resp")
msg_id = uid("msg")
text_buf = ""
yield emit("response.created", {"type": "response.created",
"response": {"id": resp_id, "object": "response", "model": model,
"status": "in_progress", "created": int(time.time()), "output": []}})
yield emit("response.in_progress", {"type": "response.in_progress", "response": {"id": resp_id}})
yield emit("response.output_item.added", {"type": "response.output_item.added",
"item": {"type": "message", "id": msg_id, "role": "assistant", "status": "in_progress", "content": []}})
yield emit("response.content_part.added", {"type": "response.content_part.added",
"part": {"type": "output_text", "text": "", "annotations": []}, "item_id": msg_id})
total_usage = {}
for raw in cc_stream:
line = raw.decode("utf-8", errors="replace").strip()
if not line:
continue
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
t = d.get("type", "")
if t == "text-delta":
txt = d.get("text", "")
if txt:
text_buf += txt
yield emit("response.output_text.delta", {"type": "response.output_text.delta",
"delta": txt, "item_id": msg_id, "content_index": 0})
elif t == "finish-step":
u = d.get("usage", {})
total_usage = {
"input_tokens": u.get("inputTokens", 0),
"output_tokens": u.get("outputTokens", 0),
"total_tokens": u.get("inputTokens", 0) + u.get("outputTokens", 0),
}
if text_buf:
yield emit("response.output_text.done", {"type": "response.output_text.done",
"text": text_buf, "item_id": msg_id, "content_index": 0})
yield emit("response.content_part.done", {"type": "response.content_part.done",
"part": {"type": "output_text", "text": text_buf, "annotations": []}, "item_id": msg_id})
yield emit("response.output_item.done", {"type": "response.output_item.done",
"item": {"type": "message", "id": msg_id, "role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": text_buf, "annotations": []}]}})
final_out = []
if text_buf:
final_out.append({"type": "message", "id": msg_id, "role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": text_buf, "annotations": []}]})
yield emit("response.completed", {"type": "response.completed",
"response": {"id": resp_id, "object": "response", "model": model,
"status": "completed", "created": int(time.time()), "output": final_out,
"usage": total_usage}})
# ═══════════════════════════════════════════════════════════════════
# HTTP Server
# ═══════════════════════════════════════════════════════════════════
@@ -531,6 +645,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
if BACKEND == "anthropic":
self._handle_anthropic(body, model, stream)
elif BACKEND == "command-code":
self._handle_command_code(body, model, stream)
else:
self._handle_openai_compat(body, model, stream)
@@ -538,6 +654,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
input_data = body.get("input", "")
messages = oa_input_to_messages(input_data)
instructions = body.get("instructions", "").strip()
if not instructions and BASE_INSTRUCTIONS:
instructions = BASE_INSTRUCTIONS
if instructions:
messages.insert(0, {"role": "system", "content": instructions})
chat_body = {"model": model, "messages": messages}
@@ -571,6 +689,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
an_body = {"model": model, "messages": an_input_to_messages(input_data),
"max_tokens": body.get("max_output_tokens", 8192)}
instructions = body.get("instructions", "").strip()
if not instructions and BASE_INSTRUCTIONS:
instructions = BASE_INSTRUCTIONS
if instructions:
an_body["system"] = instructions
for k in ("temperature", "top_p"):
@@ -601,6 +721,76 @@ class Handler(http.server.BaseHTTPRequestHandler):
lambda r: an_resp_to_responses(json.loads(r.read()), model),
lambda s: an_stream_to_sse(s, model, body.get("request_id") or body.get("id")))
def _handle_command_code(self, body, model, stream):
input_data = body.get("input", "")
instructions = body.get("instructions", "").strip()
messages = cc_input_to_messages(input_data)
if not instructions and BASE_INSTRUCTIONS:
instructions = BASE_INSTRUCTIONS
if instructions:
sys_msg = {"role": "system", "content": instructions}
messages.insert(0, sys_msg)
cc_body = {
"config": _cc_config(),
"memory": "",
"taste": "",
"skills": "",
"params": {
"stream": True,
"max_tokens": body.get("max_output_tokens", 64000),
"temperature": body.get("temperature", 0.3),
"messages": messages,
"model": model,
"tools": cc_convert_tools(body.get("tools")) or [],
},
"threadId": body.get("request_id") or uid("thread"),
}
target = upstream_target(TARGET_URL, "/alpha/generate")
fwd = forwarded_headers(self.headers, {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
"Accept": "text/event-stream, application/json",
}, browser_ua=True)
print(f"[translate-proxy] POST {target} model={model} stream={stream} [command-code]", file=sys.stderr)
req = urllib.request.Request(
target,
data=json.dumps(cc_body).encode(),
headers=fwd,
)
if stream:
try:
upstream = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
err = e.read().decode()
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
except Exception as e:
return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.end_headers()
for event in cc_stream_to_sse(upstream, model, body.get("request_id") or body.get("id")):
self.wfile.write(event.encode("utf-8"))
self.wfile.flush()
else:
try:
upstream = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
err = e.read().decode()
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": err}})
except Exception as e:
return self.send_json(500, {"error": {"type": "proxy_error", "message": str(e)}})
raw = upstream.read().decode()
lines = raw.strip().split("\n")
result = cc_resp_to_responses(lines, model)
self.send_json(200, result)
def _forward(self, req, stream, model, nonstream_fn, stream_fn):
try:
upstream = urllib.request.urlopen(req)