Skip to content
Open
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
51 changes: 38 additions & 13 deletions ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,11 @@ def _render_line_impl(self, line: str, in_code_block: bool) -> tuple[str, bool]:
if stripped.startswith("```"):
new_state = not in_code_block
lang = stripped[3:].strip()

# Use dynamic cached width to respect terminal resizes
term_cols = _get_term_cols()
bar_len = min(term_cols - 4, 60)

if new_state: # Opening a code block
lbl = lang.upper() or 'CODE'
dashes = max(1, bar_len - 17 - len(lbl))
Expand Down Expand Up @@ -460,6 +460,29 @@ def preview_line(self, line: str) -> str:

VALID_PROVIDERS = list(ENDPOINTS.keys())

# Deprecated Gemini models (as of June 1, 2026)
DEPRECATED_GEMINI_MODELS = (
"gemini-2.0-flash",
"gemini-2.0-flash-001",
"gemini-2.0-flash-lite",
"gemini-2.0-flash-lite-001",
"gemini-3-pro-preview",
"gemini-3.1-flash-lite-preview",
)

def _is_model_deprecated(model_name: str, provider: str) -> bool:
if provider != "gemini":
return False
return model_name.lower() in [m.lower() for m in DEPRECATED_GEMINI_MODELS]

def _mark_deprecated_models(models: list[str], provider: str) -> list[str]:
if provider != "gemini":
return models
return [
f"{m} {C.ERROR}[DEPRECATED]{C.RESET}" if _is_model_deprecated(m, provider) else m
for m in models
]


# ─────────────────────────────────────────────────────────────────────────────
# HELPERS
Expand Down Expand Up @@ -495,7 +518,7 @@ def check_placeholder_key(key: str, provider: str) -> bool:
bad = "looks incomplete"
elif provider == "cloudflare" and ":" not in key:
bad = "is missing the Account ID (Format must be ACCOUNT_ID:API_TOKEN)"

if bad:
eprint(f"{C.WARN}WARNING: API key for {provider.upper()} {bad}.{C.RESET}")
return False
Expand Down Expand Up @@ -826,7 +849,7 @@ def _fetch_page(url: str, timeout: int = 20) -> str:
cl_int = int(cl)
except ValueError:
cl_int = 0

if cl_int > FETCH_MAX_BYTES:
raise RuntimeError(f"Response too large: {cl_int} bytes")
raw = resp.read(FETCH_MAX_BYTES + 1)
Expand Down Expand Up @@ -1480,7 +1503,7 @@ def _eval_node(node: ast.AST) -> Any:
# Only replace ^ if it's not being used as a valid Python BitXor operator
if "^" in expression and "**" not in expression:
expression = expression.replace("^", "**")

tree = ast.parse(expression, mode="eval")
result = _eval_node(tree.body)
if isinstance(result, float) and result == int(result) and not math.isinf(result):
Expand Down Expand Up @@ -2034,7 +2057,9 @@ def fetch_models(provider: str, api_key: str) -> Optional[list[str]]:
eprint(f"{C.ERROR}Could not parse model list: {exc}{C.RESET}")
return None

return[m for m in models if m]
models = [m for m in models if m]
models = _mark_deprecated_models(models, provider)
return models


_PICK_SEL_BG = "\033[48;5;215m"
Expand Down Expand Up @@ -2217,16 +2242,16 @@ def _interactive_confirm(prompt: str, default: bool = True, color: str = C.INFO)

selected = default
_stdout_write("\033[?25l") # Hide cursor safely

try:
with _RawTerminal():
while True:
yes_str = f"{_PICK_SEL_BG}{_PICK_SEL_FG} Yes {C.RESET}" if selected else " Yes "
no_str = f"{_PICK_SEL_BG}{_PICK_SEL_FG} No {C.RESET}" if not selected else " No "

# Write inline prompt
_stdout_write(f"\r{C.CLR}{color}✦ {prompt}{C.RESET} {yes_str} {no_str}")

key = _read_picker_key()
if key in ("LEFT", "UP", "RIGHT", "DOWN", "h", "l", "j", "k"):
selected = not selected
Expand All @@ -2240,11 +2265,11 @@ def _interactive_confirm(prompt: str, default: bool = True, color: str = C.INFO)
break
elif key == "ESC":
sys.exit(0)

ans_str = "Yes" if selected else "No"
_stdout_write(f"\r{C.CLR}{color}✓ {prompt}{C.RESET} {C.BOLD}{ans_str}{C.RESET}\n")
return selected

except KeyboardInterrupt:
_stdout_write("\r")
sys.exit(0)
Expand Down Expand Up @@ -2279,7 +2304,7 @@ def _render_provider_picker(providers: list[str], selected: int) -> None:
"novita": "Novita",
"ollama": "Ollama"
}.get(p, p.title())

row = f" {num}. {display_name}"
if idx == selected:
lines.append(f"│{_PICK_SEL_BG}{_PICK_SEL_FG}{_ansi_pad(row, inner_width)}{C.RESET}│")
Expand Down Expand Up @@ -2882,7 +2907,7 @@ def feed_thinking(self, think_tok: str) -> None:
if not self._in_think_display:
_stdout_write(f"{C.THINK}[Thinking]\n┃ {C.RESET}{C.THINK}")
self._in_think_display = True

indented_think = think_tok.replace("\n", f"\n{C.THINK}┃ {C.RESET}{C.THINK}")
_stdout_write(f"{C.THINK}{indented_think}{C.RESET}")

Expand Down
46 changes: 43 additions & 3 deletions ai.sh
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,46 @@ regex_escape_ere() {
printf '%s' "$1" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g'
}

# Deprecated Gemini models (as of June 1, 2026)
DEPRECATED_GEMINI_MODELS=(
"gemini-2.0-flash"
"gemini-2.0-flash-001"
"gemini-2.0-flash-lite"
"gemini-2.0-flash-lite-001"
"gemini-3-pro-preview"
"gemini-3.1-flash-lite-preview"
)

is_model_deprecated() {
local model="$1"
local provider="$2"

if [[ "$provider" != "gemini" ]]; then
return 1
fi

local model_lower
model_lower="$(printf '%s' "$model" | tr '[:upper:]' '[:lower:]')"

for deprecated in "${DEPRECATED_GEMINI_MODELS[@]}"; do
if [[ "$model_lower" == "$deprecated" ]]; then
return 0
fi
done
return 1
}

model_display_name() {
local model="$1"
local provider="$2"

if is_model_deprecated "$model" "$provider"; then
printf '%s %s[DEPRECATED]%s' "$model" "$COLOR_ERROR" "$COLOR_RESET"
else
printf '%s' "$model"
fi
}

# --- Image Helper Functions ---
validate_image_file() {
local file_path="$1"
Expand Down Expand Up @@ -589,11 +629,11 @@ MODEL_ID=""
# --- Auto-select if only one model, otherwise prompt user ---
if [ ${#available_models[@]} -eq 1 ]; then
MODEL_ID="${available_models[0]}"
echo -e "${COLOR_INFO}Auto-selecting only matching model.${COLOR_RESET}"
echo -e "${COLOR_INFO}Auto-selecting only matching model:${COLOR_RESET} $(model_display_name "$MODEL_ID" "$PROVIDER")"
else
echo -e "${COLOR_INFO}Available Models for ${PROVIDER^^}:${COLOR_RESET}"
for i in "${!available_models[@]}"; do
printf " ${COLOR_BOLD}%3d${COLOR_RESET}. %s\n" $((i+1)) "${available_models[$i]}"
printf " ${COLOR_BOLD}%3d${COLOR_RESET}. %s\n" $((i+1)) "$(model_display_name "${available_models[$i]}" "$PROVIDER")"
done
echo ""
while true; do
Expand All @@ -607,7 +647,7 @@ else
done
fi

echo -e "${COLOR_INFO}Using model:${COLOR_RESET} ${MODEL_ID}"
echo -e "${COLOR_INFO}Using model:${COLOR_RESET} $(model_display_name "$MODEL_ID" "$PROVIDER")"
echo ""

CHAT_API_URL=""
Expand Down