diff --git a/contrib/ccyproxy/ccyproxy.zsh b/contrib/ccyproxy/ccyproxy.zsh index 531da1f7adc..894a333f949 100644 --- a/contrib/ccyproxy/ccyproxy.zsh +++ b/contrib/ccyproxy/ccyproxy.zsh @@ -22,7 +22,7 @@ _ccyproxy_unset_provider_env() { # [--list-models|--list-routes|--list-all|--show-config] [claude arguments...] function ccyproxy { local proxy_config="${CCYPROXY_CONFIG:-${HOME}/Library/Application Support/AIUsage/CLIProxyAPI/config.yaml}" - local proxy_host proxy_port proxy_key proxy_base proxy_models proxy_catalog + local proxy_host proxy_port proxy_key proxy_base proxy_models proxy_catalog proxy_raw_ids local proxy_routes proxy_route_config proxy_model proxy_requested_model proxy_haiku_model local proxy_context_window proxy_max_output_tokens proxy_translation_margin proxy_provider_compact_limit local proxy_output_allowance proxy_summary_reserve proxy_compact_trigger proxy_auto_compact_window @@ -35,6 +35,30 @@ function ccyproxy { return 1 fi + local council_command + if (( $+commands[reason-assembly] )); then + council_command=reason-assembly + elif (( $+commands[ccycouncil] )); then + council_command=ccycouncil + if [[ -z "${_REASON_ASSEMBLY_LEGACY_NOTICE:-}" ]]; then + print -u2 -- "ccyproxy: warning: using deprecated ccycouncil fallback; install reason-assembly" + typeset -g _REASON_ASSEMBLY_LEGACY_NOTICE=1 + fi + else + print -u2 -- "ccyproxy: warning: reason-assembly is unavailable; continuing with live raw-model filtering" + council_command= + fi + + if [[ "$council_command" == ccycouncil ]]; then + if ! REASON_ASSEMBLY_SUPPRESS_DEPRECATION=1 CCYPROXY_CONFIG="$proxy_config" command ccycouncil sync --json >/dev/null; then + print -u2 -- "ccyproxy: warning: catalogue synchronization failed; continuing with live raw-model filtering" + fi + elif [[ "$council_command" == reason-assembly ]]; then + if ! CCYPROXY_CONFIG="$proxy_config" command reason-assembly sync --json >/dev/null; then + print -u2 -- "ccyproxy: warning: catalogue synchronization failed; continuing with live raw-model filtering" + fi + fi + local -a proxy_connection proxy_connection=("${(@f)$(ruby -ryaml -e ' c = YAML.safe_load(File.read(ARGV.fetch(0)), aliases: true) @@ -72,14 +96,20 @@ function ccyproxy { echo "ccyproxy: CLIProxyAPI model metadata is unavailable at $proxy_base" >&2 return 1 } + proxy_raw_ids="$(printf '%s' "$proxy_models" | ruby -rjson -e ' + puts JSON.generate(JSON.parse(STDIN.read).fetch("data", []).map { |m| m["id"] }.compact.uniq.sort) + ' 2>/dev/null)" || proxy_raw_ids='[]' case "${1:-}" in --list-models) - printf '%s' "$proxy_catalog" | ruby -rjson -e ' + printf '%s' "$proxy_catalog" | PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) models = JSON.parse(STDIN.read).fetch("models", []) - models.sort_by { |m| m["slug"].to_s }.each do |m| + by_slug = models.each_with_object({}) { |m, out| out[m["slug"]] = m } + raw_ids.each do |slug| + m = by_slug[slug] || {"slug" => slug} availability = m["credential_availability"] || {} - status = availability["status"] || "unknown" + status = by_slug[slug] ? (availability["status"] || "unknown") : "listed-only" eligible = availability.key?("eligible_credentials") ? "#{availability["eligible_credentials"]}/#{availability["total_credentials"]}" : "-" puts "%-48s %10s %-11s %s" % [m["slug"], m["context_window"] || m["max_context_window"] || "unknown", status, eligible] end @@ -98,11 +128,14 @@ function ccyproxy { puts JSON.parse(ENV.fetch("PROXY_ROUTES")).map { |id| " #{id}" } ' printf 'Models:\n' - printf '%s' "$proxy_catalog" | ruby -rjson -e ' + printf '%s' "$proxy_catalog" | PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) models = JSON.parse(STDIN.read).fetch("models", []) - models.sort_by { |m| m["slug"].to_s }.each do |m| + by_slug = models.each_with_object({}) { |m, out| out[m["slug"]] = m } + raw_ids.each do |slug| + m = by_slug[slug] || {"slug" => slug} availability = m["credential_availability"] || {} - status = availability["status"] || "unknown" + status = by_slug[slug] ? (availability["status"] || "unknown") : "listed-only" eligible = availability.key?("eligible_credentials") ? "#{availability["eligible_credentials"]}/#{availability["total_credentials"]}" : "-" puts " %-46s %10s %-11s %s" % [m["slug"], m["context_window"] || m["max_context_window"] || "unknown", status, eligible] end @@ -153,8 +186,9 @@ function ccyproxy { proxy_requested_model="$proxy_model" proxy_model="$(printf '%s' "$proxy_catalog" | - PROXY_MODEL="$proxy_model" PROXY_ROUTE_CONFIG="$proxy_route_config" ruby -rjson -e ' - models = JSON.parse(STDIN.read).fetch("models", []) + PROXY_MODEL="$proxy_model" PROXY_ROUTE_CONFIG="$proxy_route_config" PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) + models = JSON.parse(STDIN.read).fetch("models", []).select { |m| raw_ids.include?(m["slug"]) } by_slug = models.each_with_object({}) { |m, out| out[m["slug"]] = m } selected = ENV.fetch("PROXY_MODEL") if by_slug[selected] @@ -176,8 +210,9 @@ function ccyproxy { fi if [[ -z "$proxy_haiku_model" ]]; then - proxy_haiku_model="$(printf '%s' "$proxy_catalog" | PROXY_MAIN_MODEL="$proxy_model" ruby -rjson -e ' - models = JSON.parse(STDIN.read).fetch("models", []) + proxy_haiku_model="$(printf '%s' "$proxy_catalog" | PROXY_MAIN_MODEL="$proxy_model" PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) + models = JSON.parse(STDIN.read).fetch("models", []).select { |m| raw_ids.include?(m["slug"]) } by_slug = models.each_with_object({}) { |m, out| out[m["slug"]] = m } preferred = %w[gpt-5.6-luna gpt-5.4-mini gemini-3.1-flash-lite gemini-3-flash] selected = preferred.find do |slug| @@ -203,8 +238,9 @@ function ccyproxy { fi proxy_availability_error="$(printf '%s' "$proxy_catalog" | - PROXY_MODELS="${proxy_model},${proxy_haiku_model}" PROXY_ROUTE_CONFIG="$proxy_route_config" ruby -rjson -rtime -e ' - catalog = JSON.parse(STDIN.read).fetch("models", []) + PROXY_MODELS="${proxy_model},${proxy_haiku_model}" PROXY_ROUTE_CONFIG="$proxy_route_config" PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -rtime -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) + catalog = JSON.parse(STDIN.read).fetch("models", []).select { |m| raw_ids.include?(m["slug"]) } by_slug = catalog.each_with_object({}) { |m, out| out[m["slug"]] = m } routes = JSON.parse(ENV.fetch("PROXY_ROUTE_CONFIG")) resolve_all = lambda do |selected, seen = []| @@ -263,8 +299,9 @@ function ccyproxy { local -a proxy_capacity proxy_capacity=("${(@f)$(printf '%s' "$proxy_catalog" | - PROXY_MODEL="$proxy_model" ruby -rjson -e ' - model = JSON.parse(STDIN.read).fetch("models", []).find { |m| m["slug"] == ENV.fetch("PROXY_MODEL") } + PROXY_MODEL="$proxy_model" PROXY_RAW_IDS="$proxy_raw_ids" ruby -rjson -e ' + raw_ids = JSON.parse(ENV.fetch("PROXY_RAW_IDS")) + model = JSON.parse(STDIN.read).fetch("models", []).find { |m| m["slug"] == ENV.fetch("PROXY_MODEL") && raw_ids.include?(m["slug"]) } exit 1 unless model puts model["context_window"].to_i puts model["max_output_tokens"].to_i diff --git a/contrib/ccyproxy/test.sh b/contrib/ccyproxy/test.sh index 0e66ffb0d9e..a13d5749abc 100755 --- a/contrib/ccyproxy/test.sh +++ b/contrib/ccyproxy/test.sh @@ -63,6 +63,25 @@ esac EOF chmod +x "$workdir/bin/curl" +cat >"$workdir/bin/reason-assembly" <<'EOF' +#!/bin/sh +set -eu +if [ -n "${MOCK_SYNC_LOG:-}" ]; then + printf 'reason-assembly %s\n' "$*" >>"$MOCK_SYNC_LOG" +fi +EOF +cat >"$workdir/bin/ccycouncil" <<'EOF' +#!/bin/sh +set -eu +if [ -n "${MOCK_SYNC_LOG:-}" ]; then + printf 'ccycouncil %s suppress=%s\n' "$*" "${REASON_ASSEMBLY_SUPPRESS_DEPRECATION:-}" >>"$MOCK_SYNC_LOG" +fi +if [ "${REASON_ASSEMBLY_SUPPRESS_DEPRECATION:-}" != 1 ]; then + printf '%s\n' 'deprecated: use reason-assembly' >&2 +fi +EOF +chmod +x "$workdir/bin/reason-assembly" "$workdir/bin/ccycouncil" + cat >"$workdir/config.yaml" <<'EOF' host: 127.0.0.1 port: 8317 @@ -86,10 +105,11 @@ run_launcher() { models=$1 catalog=$2 shift 2 - PATH="$workdir/bin:$PATH" \ + PATH="$workdir/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ CCYPROXY_CONFIG="$workdir/config.yaml" \ MOCK_MODELS="$models" \ MOCK_CATALOG="$catalog" \ + MOCK_SYNC_LOG="${MOCK_SYNC_LOG:-}" \ zsh -c 'source "$1"; shift; ccyproxy "$@"' -- "$launcher" "$@" } @@ -98,6 +118,18 @@ printf '%s\n' "$complete_output" | grep -q '^main_model=gpt-5.6-sol$' printf '%s\n' "$complete_output" | grep -q '^compact_trigger=287000$' printf '%s\n' "$complete_output" | grep -q '^auto_compact_window=320000$' +cat >"$workdir/catalog-with-phantom.json" <<'EOF' +{"models":[{"slug":"gpt-5.6-sol","context_window":372000,"max_output_tokens":128000,"translation_margin_tokens":52000,"auto_compact_token_limit":287000,"capacity_complete":true,"capacity_source":"test","capacity_blockers":[],"credential_availability":{"status":"available","availability_complete":true,"total_credentials":1,"eligible_credentials":1,"cooling_credentials":0,"blocked_credentials":0,"availability_blockers":[]}},{"slug":"gpt-5.6-luna","context_window":200000,"max_output_tokens":64000,"translation_margin_tokens":13000,"auto_compact_token_limit":160000,"capacity_complete":true,"capacity_source":"test","capacity_blockers":[],"credential_availability":{"status":"available","availability_complete":true,"total_credentials":1,"eligible_credentials":1,"cooling_credentials":0,"blocked_credentials":0,"availability_blockers":[]}}]} +EOF +phantom_output=$(run_launcher "$workdir/models-direct.json" "$workdir/catalog-with-phantom.json" --show-config) +printf '%s\n' "$phantom_output" | grep -q '^haiku_model=gpt-5.6-sol$' +listed_models=$(run_launcher "$workdir/models-direct.json" "$workdir/catalog-with-phantom.json" --list-models) +printf '%s\n' "$listed_models" | grep -q '^gpt-5.6-sol ' +if printf '%s\n' "$listed_models" | grep -q '^gpt-5.6-luna '; then + printf '%s\n' 'launcher test: metadata-only model leaked through raw catalogue filter' >&2 + exit 1 +fi + cat >"$workdir/catalog-invalid-margin.json" <<'EOF' {"models":[{"slug":"gpt-5.6-sol","context_window":372000,"max_output_tokens":128000,"capacity_complete":true,"capacity_source":"test","capacity_blockers":[],"credential_availability":{"status":"available","availability_complete":true,"total_credentials":1,"eligible_credentials":1,"cooling_credentials":0,"blocked_credentials":0,"availability_blockers":[]}}]} EOF @@ -165,3 +197,26 @@ if run_launcher "$workdir/models-route.json" "$workdir/catalog-route-incomplete. exit 1 fi grep -q "route 'worker' has no currently eligible candidate" "$workdir/route-incomplete.err" + +: >"$workdir/sync.log" +MOCK_SYNC_LOG="$workdir/sync.log" run_launcher "$workdir/models-direct.json" "$workdir/catalog-complete.json" --show-config >"$workdir/canonical-sync.out" 2>"$workdir/canonical-sync.err" +grep -q '^reason-assembly sync --json$' "$workdir/sync.log" +if grep -q '^ccycouncil ' "$workdir/sync.log"; then + printf '%s\n' 'launcher test: deprecated command ran despite canonical command availability' >&2 + exit 1 +fi + +rm "$workdir/bin/reason-assembly" +: >"$workdir/sync.log" +MOCK_SYNC_LOG="$workdir/sync.log" run_launcher "$workdir/models-direct.json" "$workdir/catalog-complete.json" --show-config >"$workdir/compat-sync.out" 2>"$workdir/compat-sync.err" +grep -q '^ccycouncil sync --json suppress=1$' "$workdir/sync.log" +grep -q 'using deprecated ccycouncil fallback' "$workdir/compat-sync.err" +if grep -q 'deprecated: use reason-assembly' "$workdir/compat-sync.err"; then + printf '%s\n' 'launcher test: compatibility wrapper warning was not suppressed' >&2 + exit 1 +fi + +rm "$workdir/bin/ccycouncil" +run_launcher "$workdir/models-direct.json" "$workdir/catalog-complete.json" --show-config >"$workdir/no-sync.out" 2>"$workdir/no-sync.err" +grep -q '^main_model=gpt-5.6-sol$' "$workdir/no-sync.out" +grep -q 'reason-assembly is unavailable; continuing with live raw-model filtering' "$workdir/no-sync.err"