v3.3.0: Antigravity OAuth + Gemini CLI OAuth, full Codex agent loop with tool calls, history hardening, SSE fixes

This commit is contained in:
Roman
2026-05-20 21:44:33 +04:00
Unverified
parent b060706e18
commit e2f20810f0
6 changed files with 1085 additions and 87 deletions

View File

@@ -4,8 +4,9 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib
import subprocess, os, signal, sys, threading, time, json, urllib.request, tempfile, shutil
import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, tempfile, shutil
import hashlib, socket, contextlib
import base64, secrets
from pathlib import Path
HOME = Path.home()
@@ -25,6 +26,21 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.3.0", "2026-05-20", [
"Added Google Antigravity OAuth backend with Code Assist endpoints and model alias mapping",
"Added Gemini CLI OAuth backend using public Gemini CLI OAuth client",
"Antigravity now creates files via tool calls — full Codex agent loop with Gemini-style history hardening",
"Fixed tool-call streaming: function_call_arguments delta/done events, thought signatures, functionResponse name matching",
"Added Endpoint Doctor, adaptive BGP scoring, provider policies, adaptive compaction, log redaction",
]),
("3.1.0", "2026-05-20", [
"Initial Antigravity/Gemini CLI OAuth split, history hardening, SSE fixes",
]),
("3.0.0", "2026-05-20", [
"ThreadingHTTPServer with dynamic proxy ports and health-gated Codex launch",
"Atomic config writes, safe cleanup registry, graceful shutdown, and buffered SSE streaming",
"Usage Dashboard v2, TCP_NODELAY streaming, Anthropic prompt caching, and batched usage stats",
]),
("2.6.1", "2026-05-20", [
"Google OAuth rebuilt to emulate Gemini CLI — no client_secret.json needed",
"Uses Google's public OAuth client_id (same as gemini-cli)",
@@ -226,13 +242,25 @@ PROVIDER_PRESETS = {
],
},
"Google Gemini (OAuth)": {
"backend_type": "openai-compat",
"base_url": "https://generativelanguage.googleapis.com/v1beta/openai",
"oauth_provider": "google",
"backend_type": "gemini-oauth-cli",
"base_url": "https://cloudcode-pa.googleapis.com",
"oauth_provider": "google-cli",
"models": [
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-2.0-flash", "gemini-2.0-flash-lite",
"gemini-2.5-flash-preview-native-audio-dialog",
],
},
"Google Antigravity (OAuth)": {
"backend_type": "gemini-oauth-antigravity",
"base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com",
"oauth_provider": "google-antigravity",
"models": [
"antigravity-gemini-3-flash",
"antigravity-gemini-3-pro",
"antigravity-gemini-3.1-pro",
"antigravity-claude-sonnet-4-6",
"antigravity-claude-opus-4-6-thinking",
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
],
},
"OpenAdapter": {
@@ -301,8 +329,10 @@ def apply_provider_preset(endpoint, preset_name):
updated["base_url"] = normalize_base_url(preset["base_url"])
if preset.get("cc_version") and not updated.get("cc_version"):
updated["cc_version"] = preset["cc_version"]
if not updated.get("models"):
if not updated.get("models") or (preset.get("backend_type") or "").startswith("gemini-oauth"):
updated["models"] = list(preset.get("models", []))
if preset.get("oauth_provider"):
updated["oauth_provider"] = preset["oauth_provider"]
if not updated.get("default_model") and updated.get("models"):
updated["default_model"] = updated["models"][0]
return updated
@@ -630,6 +660,18 @@ def _start_proxy_for(endpoint, logfn):
port = _pick_free_port()
_proxy_port = port
model_list = endpoint.get("models", [])
if (endpoint.get("backend_type") or "").startswith("gemini-oauth") and (endpoint.get("oauth_provider") or "").startswith("google"):
token_name = "google-antigravity-oauth-token.json" if endpoint.get("oauth_provider") == "google-antigravity" else "google-cli-oauth-token.json"
token_path = os.path.expanduser(f"~/.cache/codex-proxy/{token_name}")
try:
with open(token_path) as tf:
td = json.load(tf)
discovered = [] if endpoint.get("oauth_provider") == "google-antigravity" else td.get("available_models", [])
if discovered:
model_list = discovered
except Exception:
pass
pcfg = {
"port": port,
"backend_type": endpoint["backend_type"],
@@ -640,7 +682,7 @@ def _start_proxy_for(endpoint, logfn):
"reasoning_enabled": endpoint.get("reasoning_enabled", True),
"reasoning_effort": endpoint.get("reasoning_effort", "medium"),
"models": [{"id": m, "object": "model", "created": 1700000000, "owned_by": endpoint["name"]}
for m in endpoint.get("models", [])],
for m in model_list],
}
pcfg_path = PROXY_CONFIG_DIR / f"proxy-{safe_name(endpoint['name'])}-{port}.json"
pcfg_path.parent.mkdir(parents=True, exist_ok=True)
@@ -763,7 +805,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 v3.0.0</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v3.3.0</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1277,7 +1319,7 @@ class LauncherWin(Gtk.Window):
self.log("ERROR: no model selected")
return
is_bgp = name.startswith("🔀 ")
is_bgp = bool(name and name.startswith("🔀 "))
if is_bgp:
pool_name = name[2:]
pool = None
@@ -1781,6 +1823,8 @@ class EditEndpointDialog(Gtk.Dialog):
for val, lab in [("openai-compat", "OpenAI-compatible (needs proxy)"),
("anthropic", "Anthropic (needs proxy)"),
("command-code", "Command Code (needs proxy)"),
("gemini-oauth-cli", "Gemini CLI OAuth (needs proxy)"),
("gemini-oauth-antigravity", "Antigravity OAuth (needs proxy)"),
("native", "Native OpenAI (no proxy)")]:
self._combo_type.append(val, lab)
bt = self._data.get("backend_type", "openai-compat")
@@ -1866,6 +1910,9 @@ class EditEndpointDialog(Gtk.Dialog):
self._fetch_models_btn = Gtk.Button(label="Fetch from API")
self._fetch_models_btn.connect("clicked", lambda b: self._fetch_models())
mbox.pack_start(self._fetch_models_btn, False, False, 0)
self._test_btn = Gtk.Button(label="Test Endpoint")
self._test_btn.connect("clicked", lambda b: self._diagnose_endpoint())
mbox.pack_start(self._test_btn, False, False, 0)
bulk_lbl = Gtk.Label(label="Bulk add models (one per line or comma-separated):", xalign=0)
area.pack_start(bulk_lbl, False, False, 2)
@@ -1970,29 +2017,52 @@ class EditEndpointDialog(Gtk.Dialog):
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
provider = preset.get("oauth_provider", "")
if provider == "google":
self._google_oauth_flow()
if (provider or "").startswith("google"):
self._google_oauth_flow(provider)
def _google_oauth_flow(self):
token_path = os.path.expanduser("~/.cache/codex-proxy/google-oauth-token.json")
def _google_oauth_flow(self, oauth_provider="google-cli"):
is_antigravity = oauth_provider == "google-antigravity"
token_path = os.path.expanduser("~/.cache/codex-proxy/google-antigravity-oauth-token.json" if is_antigravity else "~/.cache/codex-proxy/google-cli-oauth-token.json")
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
CLIENT_SECRET = "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w"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/generative-language.retriever",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
]
import http.server, hashlib, secrets, socket
if is_antigravity:
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
CLIENT_SECRET = "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"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
]
port = 51121
redirect_uri = f"http://localhost:{port}/oauth-callback"
callback_path = "/oauth-callback"
provider_kind = "antigravity"
else:
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
CLIENT_SECRET = "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"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
]
port = 0
redirect_uri = None
callback_path = "/oauth2callback"
provider_kind = "cli"
import http.server
port = 8085
state = secrets.token_hex(32)
verifier = secrets.token_urlsafe(32)
challenge = hashlib.sha256(verifier.encode()).digest()
challenge_b64 = urllib.parse.quote_plus(__import__('base64').urlsafe_b64encode(challenge).rstrip(b'=').decode())
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
if port == 0:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
redirect_uri = f"http://127.0.0.1:{port}/oauth2callback"
redirect_uri = f"http://127.0.0.1:{port}/oauth2callback"
scope_str = " ".join(SCOPES)
auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
@@ -2001,9 +2071,9 @@ class EditEndpointDialog(Gtk.Dialog):
f"&response_type=code"
f"&scope={urllib.parse.quote(scope_str)}"
f"&access_type=offline"
f"&prompt=consent"
f"&prompt=select_account%20consent"
f"&state={state}"
f"&code_challenge={challenge_b64}"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
@@ -2043,6 +2113,14 @@ class EditEndpointDialog(Gtk.Dialog):
qs = urllib.parse.urlparse(self2.path).query
params = urllib.parse.parse_qs(qs)
received_state[0] = params.get("state", [None])[0]
with open("/tmp/codex-oauth-debug.log", "a") as _dbg:
_dbg.write(f"[{time.strftime('%H:%M:%S')}] GET {self2.path} state={received_state[0]} code={'code' in params}\n")
if self2.path.find(callback_path) == -1:
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
self2.end_headers()
error_holder[0] = "unexpected request"
return
if "code" in params:
if received_state[0] != state:
self2.send_response(400)
@@ -2061,63 +2139,171 @@ class EditEndpointDialog(Gtk.Dialog):
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
self2.end_headers()
def log_message(self2, *a): pass
def log_message(self2, fmt, *args):
with open("/tmp/codex-oauth-debug.log", "a") as _dbg:
_dbg.write(f"[{time.strftime('%H:%M:%S')}] {fmt % args}\n")
try:
server = http.server.HTTPServer(("127.0.0.1", port), OAuthHandler)
bind_host = "localhost" if is_antigravity else "127.0.0.1"
server = http.server.HTTPServer((bind_host, port), OAuthHandler)
except OSError:
self._oauth_status.set_text(f"Port {port} already in use — close other apps and retry.")
spinner.stop()
dlg.run(); dlg.destroy()
return
def _oauth_log(msg):
with open("/tmp/codex-oauth-debug.log", "a") as _f:
_f.write(f"[{time.strftime('%H:%M:%S')}] {msg}\n")
_oauth_log(f"Starting OAuth: port={port} redirect_uri={redirect_uri}")
def wait_for_code():
server.handle_request()
_oauth_log("wait_for_code thread started")
deadline = time.time() + 120
while code_holder[0] is None and error_holder[0] is None and time.time() < deadline:
server.handle_request()
server.server_close()
GLib.idle_add(self._google_oauth_complete_gemini, dlg, code_holder, error_holder,
CLIENT_ID, CLIENT_SECRET, redirect_uri, token_path, spinner, verifier)
_oauth_log(f"Server closed. code={'yes' if code_holder[0] else 'no'} error={'yes' if error_holder[0] else 'no'}")
if code_holder[0]:
try:
_oauth_log("Exchanging code for token...")
token_data = urllib.parse.urlencode({
"code": code_holder[0],
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"code_verifier": verifier,
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
resp = urllib.request.urlopen(req, timeout=30)
tokens = json.loads(resp.read())
tokens["client_id"] = CLIENT_ID
tokens["client_secret"] = CLIENT_SECRET
tokens["provider_kind"] = provider_kind
tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600)
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
os.chmod(token_path, 0o600)
_oauth_log(f"Token saved to {token_path}")
project_id = ""
try:
_oauth_log("Discovering project ID via loadCodeAssist...")
lr = urllib.request.Request(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
data=json.dumps({}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {tokens['access_token']}",
"User-Agent": "google-api-nodejs-client/9.15.1",
})
lresp = urllib.request.urlopen(lr, timeout=15)
ldata = json.loads(lresp.read())
p = ldata.get("cloudaicompanionProject", "")
if isinstance(p, dict):
project_id = p.get("id", "")
elif isinstance(p, str):
project_id = p
_oauth_log(f"Project ID: {project_id or '(none)'}")
if project_id:
tokens["project_id"] = project_id
with open(token_path, "w") as f2:
json.dump(tokens, f2, indent=2)
os.chmod(token_path, 0o600)
except Exception as pe:
_oauth_log(f"loadCodeAssist failed (non-fatal): {pe}")
if is_antigravity:
found_models = [
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
"gemini-3-pro-low", "gemini-3-pro-high",
"gemini-3.1-pro-low", "gemini-3.1-pro-high",
"gemini-3-flash-low", "gemini-3-flash-medium", "gemini-3-flash-high",
"claude-sonnet-4-6", "claude-opus-4-6-thinking",
"claude-opus-4-6-thinking-low", "claude-opus-4-6-thinking-medium", "claude-opus-4-6-thinking-high",
"gemini-claude-sonnet-4-6",
"gemini-claude-opus-4-6-thinking-low", "gemini-claude-opus-4-6-thinking-medium", "gemini-claude-opus-4-6-thinking-high",
"gemini-3-pro-image",
]
probe_candidates = [
"gemini-2.5-flash", "gemini-2.5-pro",
"gemini-3-flash-preview", "gemini-3-pro-preview", "gemini-3.1-pro-preview",
]
_oauth_log(f"Probing {len(probe_candidates)} model candidates...")
for mc in probe_candidates:
try:
pr = urllib.request.Request(
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent",
data=json.dumps({
"project": project_id,
"model": mc,
"request": {"contents": [{"role": "user", "parts": [{"text": "x"}]}],
"generationConfig": {"maxOutputTokens": 1}},
}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {tokens['access_token']}",
"User-Agent": "google-api-nodejs-client/9.15.1",
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
})
pr.get_method = lambda: "POST"
resp = urllib.request.urlopen(pr, timeout=10)
resp.read()
found_models.append(mc)
_oauth_log(f" {mc} → available")
except urllib.error.HTTPError as e:
if e.code == 429:
found_models.append(mc)
_oauth_log(f" {mc} → available (rate limited)")
else:
e.read()
_oauth_log(f" {mc} → HTTP {e.code}")
except Exception as e:
_oauth_log(f" {mc} → error: {e}")
else:
found_models = ["gemini-2.5-flash", "gemini-2.5-pro"]
if found_models:
tokens["available_models"] = found_models
with open(token_path, "w") as f3:
json.dump(tokens, f3, indent=2)
os.chmod(token_path, 0o600)
_oauth_log(f"Discovered {len(found_models)} models: {found_models}")
else:
_oauth_log("No models discovered (will use defaults)")
GLib.idle_add(self._oauth_success, dlg, tokens.get("access_token", ""), spinner)
return
except urllib.error.HTTPError as e:
body = e.read().decode(errors='replace')
_oauth_log(f"Token exchange HTTP {e.code}: {body}")
GLib.idle_add(self._oauth_failed, dlg, f"Token exchange failed ({e.code}): {body[:200]}", spinner)
return
except Exception as e:
_oauth_log(f"Token exchange FAILED: {e}")
GLib.idle_add(self._oauth_failed, dlg, f"Token exchange failed: {e}", spinner)
return
_oauth_log(f"OAuth failed: {error_holder[0] or 'timeout'}")
GLib.idle_add(self._oauth_failed, dlg,
error_holder[0] or "No authorization code received.", spinner)
threading.Thread(target=wait_for_code, daemon=True).start()
subprocess.Popen(["xdg-open", auth_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
dlg.connect("response", lambda d, r: d.destroy())
dlg.run()
dlg.destroy()
def _google_oauth_complete_gemini(self, dlg, code_holder, error_holder,
client_id, client_secret, redirect_uri, token_path, spinner, verifier):
def _oauth_success(self, dlg, access_token, spinner):
spinner.stop()
if error_holder[0]:
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Error: {error_holder[0]}</span>')
return
if not code_holder[0]:
self._oauth_status.set_text("No authorization code received.")
return
self._entry_key.set_text(access_token)
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Token saved.</span>')
dlg.set_title("Google OAuth — Success")
GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK))
self._oauth_status.set_text("Exchanging code for token…")
try:
token_data = urllib.parse.urlencode({
"code": code_holder[0],
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"code_verifier": verifier,
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
resp = urllib.request.urlopen(req, timeout=30)
tokens = json.loads(resp.read())
tokens["client_id"] = client_id
tokens["client_secret"] = client_secret
tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600)
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
os.chmod(token_path, 0o600)
self._entry_key.set_text(tokens.get("access_token", ""))
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Token saved.</span>')
dlg.set_title("Google OAuth — Success")
except Exception as e:
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Token exchange failed: {e}</span>')
def _oauth_failed(self, dlg, msg, spinner):
spinner.stop()
self._oauth_status.set_markup(f'<span foreground="#e74c3c">{msg}</span>')
GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL))
def _remove_model(self, path):
current = self._combo_default.get_active_text()
@@ -2163,6 +2349,70 @@ class EditEndpointDialog(Gtk.Dialog):
return True, None
return False, err or "No models returned by endpoint"
def _diagnose_endpoint(self):
url = self._entry_url.get_text().strip()
key = self._entry_key.get_text().strip()
bt = self._combo_type.get_active_id() or "openai-compat"
model = self._combo_default.get_active_text() or ""
checks = []
def add(name, ok, detail=""):
checks.append((name, ok, detail))
parsed = urllib.parse.urlparse(url)
add("URL format", bool(parsed.scheme and parsed.netloc),
url if parsed.scheme else "Missing scheme (https://)")
try:
t0 = time.time()
ep = {"base_url": url, "api_key": key, "backend_type": bt}
ids, err = fetch_models_for_endpoint(ep)
lat = (time.time() - t0) * 1000
if ids:
add("Network reachable", True, f"{lat:.0f}ms")
add("Auth valid", True)
add("/models endpoint", True, f"{len(ids)} models in {lat:.0f}ms")
if model:
add("Selected model exists", model in ids,
model if model in ids else f"'{model}' not in {ids[:5]}...")
else:
add("Selected model", False, "No model selected")
elif err and ("401" in str(err) or "403" in str(err)):
add("Network reachable", True, f"{lat:.0f}ms")
add("Auth valid", False, str(err)[:100])
add("/models endpoint", False, "Auth failed")
else:
add("Network reachable", False, str(err or "no response")[:100])
except Exception as e:
add("Network", False, str(e)[:100])
dlg = Gtk.Dialog(title="Endpoint Doctor", parent=self, modal=True)
dlg.add_button("Close", Gtk.ResponseType.CLOSE)
dlg.set_default_size(420, 300)
area = dlg.get_content_area()
area.set_margin_start(12)
area.set_margin_end(12)
area.set_margin_top(12)
area.set_margin_bottom(12)
area.set_spacing(4)
for name, ok, detail in checks:
row = Gtk.Box(spacing=6)
icon = Gtk.Label()
icon.set_markup(f'<span foreground="{"#27ae60" if ok else "#e74c3c"}"'
f' weight="bold">{"\u2713" if ok else "\u2717"}</span>')
row.pack_start(icon, False, False, 0)
lbl = Gtk.Label()
lbl.set_markup(f'<b>{name}</b>')
row.pack_start(lbl, False, False, 0)
if detail:
det = Gtk.Label()
det.set_markup(f'<span foreground="#7f8c8d" size="small">{detail}</span>')
row.pack_end(det, False, False, 0)
area.pack_start(row, False, False, 0)
dlg.show_all()
dlg.run()
dlg.destroy()
def _on_response(self, dialog, response):
if response != Gtk.ResponseType.OK:
self.destroy()
@@ -2172,7 +2422,7 @@ class EditEndpointDialog(Gtk.Dialog):
if not name:
self._show_error("Name is required")
return
bt = self._combo_type.get_active_id()
bt = self._combo_type.get_active_id() or PROVIDER_PRESETS.get(self._combo_preset.get_active_text() or "", {}).get("backend_type") or "openai-compat"
url = self._entry_url.get_text().strip()
key = self._entry_key.get_text().strip()
models = [self._model_store[i][0] for i in range(len(self._model_store))]