oi-chat 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oi_chat-0.1.0/.env.example +12 -0
- oi_chat-0.1.0/.github/workflows/ci.yml +31 -0
- oi_chat-0.1.0/.github/workflows/release.yml +35 -0
- oi_chat-0.1.0/.gitignore +171 -0
- oi_chat-0.1.0/.pre-commit-config.yaml +15 -0
- oi_chat-0.1.0/.python-version +1 -0
- oi_chat-0.1.0/AGENTS.md +210 -0
- oi_chat-0.1.0/CHANGELOG.md +30 -0
- oi_chat-0.1.0/CLAUDE.md +1 -0
- oi_chat-0.1.0/LICENSE +21 -0
- oi_chat-0.1.0/PKG-INFO +170 -0
- oi_chat-0.1.0/README.md +134 -0
- oi_chat-0.1.0/demo.gif +0 -0
- oi_chat-0.1.0/demo.tape +39 -0
- oi_chat-0.1.0/pyproject.toml +62 -0
- oi_chat-0.1.0/src/oi/__init__.py +0 -0
- oi_chat-0.1.0/src/oi/app.py +514 -0
- oi_chat-0.1.0/src/oi/cli.py +90 -0
- oi_chat-0.1.0/src/oi/config/__init__.py +0 -0
- oi_chat-0.1.0/src/oi/config/loaders.py +231 -0
- oi_chat-0.1.0/src/oi/config/settings.py +61 -0
- oi_chat-0.1.0/src/oi/constants.py +26 -0
- oi_chat-0.1.0/src/oi/core/__init__.py +0 -0
- oi_chat-0.1.0/src/oi/core/chat_manager.py +176 -0
- oi_chat-0.1.0/src/oi/core/chat_repository.py +168 -0
- oi_chat-0.1.0/src/oi/core/client.py +249 -0
- oi_chat-0.1.0/src/oi/core/message_utils.py +175 -0
- oi_chat-0.1.0/src/oi/core/session.py +156 -0
- oi_chat-0.1.0/src/oi/core/smart_title.py +52 -0
- oi_chat-0.1.0/src/oi/exceptions.py +31 -0
- oi_chat-0.1.0/src/oi/llm_types.py +29 -0
- oi_chat-0.1.0/src/oi/local_commands.py +87 -0
- oi_chat-0.1.0/src/oi/main.py +6 -0
- oi_chat-0.1.0/src/oi/models.yaml +54 -0
- oi_chat-0.1.0/src/oi/models_template.yaml +91 -0
- oi_chat-0.1.0/src/oi/prompts/prompt_concise.txt +1 -0
- oi_chat-0.1.0/src/oi/prompts/prompt_empty.txt +0 -0
- oi_chat-0.1.0/src/oi/prompts/prompt_general.txt +1 -0
- oi_chat-0.1.0/src/oi/prompts.py +59 -0
- oi_chat-0.1.0/src/oi/registry.py +134 -0
- oi_chat-0.1.0/src/oi/renderers.py +166 -0
- oi_chat-0.1.0/src/oi/response_handler.py +149 -0
- oi_chat-0.1.0/src/oi/ui/__init__.py +0 -0
- oi_chat-0.1.0/src/oi/ui/chat_selector.py +320 -0
- oi_chat-0.1.0/src/oi/ui/image_paste.py +221 -0
- oi_chat-0.1.0/src/oi/ui/input_handler.py +138 -0
- oi_chat-0.1.0/src/oi/ui/labels.py +87 -0
- oi_chat-0.1.0/tests/__init__.py +0 -0
- oi_chat-0.1.0/tests/conftest.py +12 -0
- oi_chat-0.1.0/tests/fixtures/__init__.py +0 -0
- oi_chat-0.1.0/tests/fixtures/test_models.yaml +17 -0
- oi_chat-0.1.0/tests/integration/__init__.py +0 -0
- oi_chat-0.1.0/tests/test_main.py +17 -0
- oi_chat-0.1.0/tests/unit/__init__.py +0 -0
- oi_chat-0.1.0/tests/unit/config/__init__.py +0 -0
- oi_chat-0.1.0/tests/unit/config/test_loaders.py +262 -0
- oi_chat-0.1.0/tests/unit/core/__init__.py +0 -0
- oi_chat-0.1.0/tests/unit/core/test_chat_factory.py +25 -0
- oi_chat-0.1.0/tests/unit/core/test_client.py +226 -0
- oi_chat-0.1.0/tests/unit/core/test_message_utils.py +19 -0
- oi_chat-0.1.0/tests/unit/core/test_session.py +302 -0
- oi_chat-0.1.0/tests/unit/core/test_smart_title.py +66 -0
- oi_chat-0.1.0/tests/unit/providers/__init__.py +0 -0
- oi_chat-0.1.0/tests/unit/test_app.py +287 -0
- oi_chat-0.1.0/tests/unit/test_cli.py +96 -0
- oi_chat-0.1.0/tests/unit/test_registry.py +179 -0
- oi_chat-0.1.0/tests/unit/test_renderers.py +50 -0
- oi_chat-0.1.0/tests/unit/ui/test_chat_selector.py +61 -0
- oi_chat-0.1.0/tests/unit/ui/test_input_handler.py +74 -0
- oi_chat-0.1.0/todo.txt +24 -0
- oi_chat-0.1.0/uv.lock +3824 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# LLM CLI API Keys
|
|
2
|
+
# Copy this file to .env and add your actual API keys
|
|
3
|
+
|
|
4
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
5
|
+
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
|
6
|
+
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
|
7
|
+
XAI_API_KEY=your_xai_api_key_here
|
|
8
|
+
GEMINI_API_KEY=your_gemini_api_key_here
|
|
9
|
+
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
10
|
+
|
|
11
|
+
# Optional: Custom chat storage directory
|
|
12
|
+
# LLM_CLI_CHAT_DIR=/path/to/custom/chat/directory
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv run --python ${{ matrix.python-version }} --group dev pytest -q
|
|
21
|
+
|
|
22
|
+
lint:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: astral-sh/setup-uv@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
enable-cache: true
|
|
30
|
+
- run: uvx pre-commit run --all-files
|
|
31
|
+
- run: uv run --group dev ty check
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI on a version tag (e.g. `git tag v0.1.0 && git push --tags`).
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API tokens stored in the repo.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
- run: uv build
|
|
16
|
+
- uses: actions/upload-artifact@v4
|
|
17
|
+
with:
|
|
18
|
+
name: dist
|
|
19
|
+
path: dist/
|
|
20
|
+
|
|
21
|
+
publish:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment:
|
|
25
|
+
name: pypi
|
|
26
|
+
url: https://pypi.org/p/oi-chat
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write # required for Trusted Publishing
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/download-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
- uses: astral-sh/setup-uv@v5
|
|
35
|
+
- run: uv publish --trusted-publishing always
|
oi_chat-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
110
|
+
.pdm.toml
|
|
111
|
+
.pdm-python
|
|
112
|
+
.pdm-build/
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
163
|
+
oi/prompts/*
|
|
164
|
+
!oi/prompts/prompt_general.txt
|
|
165
|
+
!oi/prompts/prompt_concise.txt
|
|
166
|
+
|
|
167
|
+
# Temporary/scratch files
|
|
168
|
+
tmp.*
|
|
169
|
+
|
|
170
|
+
# Pre-commit cache
|
|
171
|
+
.pre-commit-cache/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-yaml
|
|
6
|
+
- id: check-added-large-files
|
|
7
|
+
- id: check-merge-conflict
|
|
8
|
+
- id: check-case-conflict
|
|
9
|
+
|
|
10
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
11
|
+
rev: v0.13.2
|
|
12
|
+
hooks:
|
|
13
|
+
- id: ruff
|
|
14
|
+
args: [--fix]
|
|
15
|
+
- id: ruff-format
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
oi_chat-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commit Messages
|
|
6
|
+
|
|
7
|
+
Use [Conventional Commits](https://www.conventionalcommits.org/) style:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
<type>(<optional scope>): <description>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Common types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
- `feat(models): add support for Gemini 2.0`
|
|
17
|
+
- `fix(client): handle retry on rate limit errors`
|
|
18
|
+
- `refactor: extract ChatSelector into ui module`
|
|
19
|
+
|
|
20
|
+
## Development Commands
|
|
21
|
+
|
|
22
|
+
**Testing:**
|
|
23
|
+
```bash
|
|
24
|
+
uv run pytest # Run all tests
|
|
25
|
+
uv run pytest tests/test_main.py # Run specific test file
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Code Quality:**
|
|
29
|
+
```bash
|
|
30
|
+
uv run ty check # Type checking (ruff formatting/linting handled by pre-commit)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Installation & Setup:**
|
|
34
|
+
```bash
|
|
35
|
+
# Local development with uv
|
|
36
|
+
uv install
|
|
37
|
+
|
|
38
|
+
# Install dev dependencies
|
|
39
|
+
uv install --group dev
|
|
40
|
+
|
|
41
|
+
# Set up pre-commit hooks
|
|
42
|
+
uv run pre-commit install
|
|
43
|
+
|
|
44
|
+
# Add new dependencies
|
|
45
|
+
uv add <package-name> # Add a new dependency
|
|
46
|
+
|
|
47
|
+
# Global installation
|
|
48
|
+
pipx install -e . # Install from local copy
|
|
49
|
+
pipx install --force -e . # Reinstall after changes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Running the application:**
|
|
53
|
+
```bash
|
|
54
|
+
uv run oi # CLI interface with default settings
|
|
55
|
+
uv run oi -P concise -m sonnet # Use specific prompt and model
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Architecture Overview (Post-Refactoring)
|
|
59
|
+
|
|
60
|
+
**Directory Structure:**
|
|
61
|
+
```
|
|
62
|
+
src/oi/
|
|
63
|
+
├── core/ # Core business logic
|
|
64
|
+
│ ├── client.py # LLMClient - API calls & retry logic
|
|
65
|
+
│ ├── session.py # Chat & ChatMetadata - data models + Chat.create_new()
|
|
66
|
+
│ ├── chat_manager.py # ChatManager - CRUD operations
|
|
67
|
+
│ ├── chat_repository.py # ChatRepository - filesystem persistence
|
|
68
|
+
│ ├── message_utils.py # Message serialization & history helpers
|
|
69
|
+
│ └── smart_title.py # Smart title generation
|
|
70
|
+
├── config/ # Configuration management
|
|
71
|
+
│ ├── settings.py # Config class + user config (JSON) management
|
|
72
|
+
│ └── loaders.py # YAML model configuration loading & merging
|
|
73
|
+
├── ui/ # User interface components
|
|
74
|
+
│ ├── input_handler.py # InputHandler - prompt_toolkit integration
|
|
75
|
+
│ ├── chat_selector.py # ChatSelector - interactive chat picker
|
|
76
|
+
│ ├── image_paste.py # PasteStore (images + long text) + PillProcessor + clipboard image reader
|
|
77
|
+
│ └── labels.py # Shared ANSI/Rich/prompt-toolkit label styling
|
|
78
|
+
├── llm_types.py # Shared chat/model capability dataclasses
|
|
79
|
+
├── app.py # Main application orchestration + ChatLoopContext
|
|
80
|
+
├── cli.py # Command-line argument parsing
|
|
81
|
+
├── main.py # Entry point (delegates to app.py)
|
|
82
|
+
├── constants.py # All constants & UI config
|
|
83
|
+
├── exceptions.py # Custom exception classes
|
|
84
|
+
├── local_commands.py # Local in-chat slash command registry + completion
|
|
85
|
+
├── prompts.py # Prompt file loading
|
|
86
|
+
├── registry.py # ModelRegistry - alias + capability management (single config load)
|
|
87
|
+
└── renderers.py # Response rendering (StyledRenderer)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Multi-provider LLM Client:**
|
|
91
|
+
Supports OpenAI, Anthropic, DeepSeek, Google Gemini, xAI, and OpenRouter through Pydantic AI's `direct` APIs with a unified interface.
|
|
92
|
+
|
|
93
|
+
**Centralized Model Registry:**
|
|
94
|
+
- `ModelRegistry` loads merged config once via `load_merged_model_config()`, then derives both the model map and capabilities from it
|
|
95
|
+
- Providers are "dumb" API clients - no hardcoded model definitions
|
|
96
|
+
- Default model configurable via `aliases.default` in YAML
|
|
97
|
+
- Cross-provider aliases supported
|
|
98
|
+
|
|
99
|
+
**Model Configuration:**
|
|
100
|
+
- **Minimal default config**: `src/oi/models.yaml` contains only latest SOTA models with date-free aliases
|
|
101
|
+
- **Auto-generated user config**: `~/.config/oi/models.yaml` created on first run from `models_template.yaml`
|
|
102
|
+
- **Deep merge**: User config merges with defaults at model property level (can add just `extra_params` without repeating all capabilities)
|
|
103
|
+
- **YAML anchors**: Top-level keys starting with `_` are ignored (prevents anchors from being treated as providers)
|
|
104
|
+
- **extra_params support**: Model-specific settings (OpenRouter quantization, OpenAI `openai_reasoning_effort`, etc.) merged into `model_settings` before API calls
|
|
105
|
+
- Per-model settings: `max_tokens`, `supports_search`, `supports_thinking`, `supports_vision`, `extra_params`
|
|
106
|
+
|
|
107
|
+
**Configuration & Prompts:**
|
|
108
|
+
Dual-location system:
|
|
109
|
+
1. User config directory (`~/.config/oi/prompts/`) - takes precedence
|
|
110
|
+
2. Package built-in prompts (`src/oi/prompts/`)
|
|
111
|
+
|
|
112
|
+
Format: `prompt_[name].txt`, loaded via `prompts.py:read_system_message_from_file()`
|
|
113
|
+
|
|
114
|
+
**Chat Management:**
|
|
115
|
+
- Rich-based interactive chat selection via `ui/chat_selector.py`
|
|
116
|
+
- Automatic session persistence with metadata in `core/session.py`
|
|
117
|
+
- Smart title generation (triggers after 8+ messages)
|
|
118
|
+
- Auto-save functionality
|
|
119
|
+
|
|
120
|
+
**Headless Mode:**
|
|
121
|
+
- `-p MESSAGE` sends one turn and exits; composes with `-c` / `-r ID` to follow up against existing chats (appends in-place, same chat ID)
|
|
122
|
+
- `--ephemeral` skips all persistence. Combined with `-c` / `-r` it runs a scratch turn against the existing chat's context without modifying it. Works in interactive mode too — the save gate is in `run_chat_loop` via `ctx.ephemeral`
|
|
123
|
+
- `run_headless_turn()` in `app.py` is the headless entry point; `main()` branches to it when `args.prompt is not None`
|
|
124
|
+
- Output cleanups for pipe-friendliness: `AI:` label hidden via `ChatOptions.show_assistant_label=False`, `Loaded chat:` / "No previous chats" chatter suppressed via `handle_chat_selection(quiet=True)`. Thinking traces still render unless `--hide-thinking` is passed (compose them for clean stdout)
|
|
125
|
+
- `-r` without an ID errors in headless — interactive selector is unavailable
|
|
126
|
+
|
|
127
|
+
**Streaming & Output:**
|
|
128
|
+
- `StyledRenderer` is the only renderer — provides styled thinking traces (NOT markdown rendering!)
|
|
129
|
+
- Shared label/color definitions live in `ui/labels.py` and are reused by plain prints, Rich output, and the prompt label
|
|
130
|
+
- Rich console with `highlight=False` to prevent number styling in LLM output
|
|
131
|
+
- Real-time streaming with interrupt handling
|
|
132
|
+
|
|
133
|
+
**Paste Pills (images + long text):**
|
|
134
|
+
- One unified `PasteStore` in `ui/image_paste.py` allocates Unicode PUA sentinel chars for both kinds of pastes; one sentinel per entry so backspace/vim `x`/word motions treat the pill atomically
|
|
135
|
+
- `PillProcessor` expands each sentinel at display time: image sentinels render as `[Image #N] `, text-paste sentinels as `[Paste #N (L lines)] `. Images and pastes are numbered independently, by first occurrence order in the buffer, so deleting one renumbers the rest
|
|
136
|
+
- Image path: `Alt+V` reads the clipboard (via `wl-paste` / `xclip`) and inserts an image sentinel. Binding is only registered when the active model has `supports_vision: true`. `Ctrl+V` is unusable because most terminals (Ghostty, Konsole, iTerm2, …) hijack it for `paste_from_clipboard`
|
|
137
|
+
- Long-text path: `Keys.BracketedPaste` is intercepted in `InputHandler`; pastes that hit `PASTE_LINE_THRESHOLD` (6 lines) **or** `PASTE_CHAR_THRESHOLD` (400 chars) become a single text-paste sentinel, shorter pastes are inserted verbatim. The char limit catches long single-line paragraphs that wrap across many rendered rows. This works around a prompt_toolkit limitation — its diff-based renderer can't progressively commit rows to the scrollback buffer, so any content that scrolls past terminal height gets permanently clobbered. Pills keep the buffer visually short. The pill label still shows lines (source lines match the user's mental model of what they copied, even though chars drive the trigger)
|
|
138
|
+
- On submit, `PasteStore.split()` walks the buffer: text-paste sentinels expand inline into the surrounding text, image sentinels become `BinaryContent` parts. Returns `str` (text-only) or `list[str | BinaryContent]` (mixed). `UserPromptPart` takes either; pydantic-ai passes through to providers
|
|
139
|
+
- `flatten_history` only needs to handle images (text pastes are just text by submit time) — renders `[Image #N]` placeholders for replay of mixed-content messages
|
|
140
|
+
|
|
141
|
+
**Local Slash Commands:**
|
|
142
|
+
- Local in-chat commands are defined in `local_commands.py`, not inline in `InputHandler`
|
|
143
|
+
- `InputHandler` wires slash command completion through prompt-toolkit
|
|
144
|
+
- Slash command completion uses `CompleteStyle.READLINE_LIKE`, so completion is `Tab`-triggered and rendered in a readline-like way instead of a dropdown menu
|
|
145
|
+
- Unknown slash commands are still rejected in `app.py` after submit so they never get sent to the model
|
|
146
|
+
|
|
147
|
+
**Key Components:**
|
|
148
|
+
- `LLMClient` (core/client.py) - High-level API client with retry logic
|
|
149
|
+
- `ChatManager` (core/chat_manager.py) - Session persistence & management
|
|
150
|
+
- `Chat`/`ChatMetadata` (core/session.py) - Data models
|
|
151
|
+
- `ChatSelector` (ui/chat_selector.py) - Interactive chat selection
|
|
152
|
+
- `InputHandler` (ui/input_handler.py) - User input handling
|
|
153
|
+
- `local_commands.py` - Slash command definitions + completion helpers
|
|
154
|
+
- `ui/labels.py` - Shared label text and styling helpers
|
|
155
|
+
- `ModelRegistry` (registry.py) - Central model/provider management
|
|
156
|
+
- `ResponseHandler` (response_handler.py) - Streaming coordination
|
|
157
|
+
|
|
158
|
+
**Main Function Structure:**
|
|
159
|
+
Located in `app.py`, broken into logical functions:
|
|
160
|
+
- `parse_arguments()` - CLI parsing (from cli.py)
|
|
161
|
+
- `setup_configuration()` - Returns a `ChatLoopContext` bundling all components
|
|
162
|
+
- `handle_chat_selection()` - Chat loading
|
|
163
|
+
- `Chat.create_new()` - New session creation (classmethod on `Chat`)
|
|
164
|
+
- `run_chat_loop(chat, ctx)` - Main interaction (takes `ChatLoopContext`)
|
|
165
|
+
- `run_headless_turn(args, ctx, registry)` - Single-turn headless path (used when `-p` is set)
|
|
166
|
+
- `main()` - High-level orchestration
|
|
167
|
+
|
|
168
|
+
**Key Constants:**
|
|
169
|
+
Mostly centralized in `constants.py`:
|
|
170
|
+
- `MIN_MESSAGES_FOR_SMART_TITLE = 8`
|
|
171
|
+
- `DEFAULT_PAGE_SIZE = 10`
|
|
172
|
+
- UI navigation keys
|
|
173
|
+
|
|
174
|
+
Conversation and status labels are centralized in `ui/labels.py`:
|
|
175
|
+
- `USER_LABEL`, `AI_LABEL`, `SYSTEM_LABEL`
|
|
176
|
+
- `INFO_LABEL`, `WARNING_LABEL`, `ERROR_LABEL`
|
|
177
|
+
|
|
178
|
+
**Common Gotchas:**
|
|
179
|
+
1. Add models to `models.yaml`, not provider classes
|
|
180
|
+
2. `StyledRenderer` is for styled thinking traces, NOT markdown rendering
|
|
181
|
+
3. Default model from YAML `aliases.default`, not hardcoded
|
|
182
|
+
4. No bespoke provider classes—add/update models via YAML aliases instead
|
|
183
|
+
5. Thinking traces:
|
|
184
|
+
- OpenAI reasoning models automatically receive `openai_reasoning_summary="detailed"` when thinking is enabled so we can render their reasoning summaries.
|
|
185
|
+
- OpenAI reasoning models also set `openai_reasoning_effort="medium"` by default to satisfy the API requirement.
|
|
186
|
+
- Anthropic models default to `anthropic_thinking={"type": "adaptive"}` when thinking is enabled (via `setdefault` in client.py). Adaptive thinking means the model decides how much to think.
|
|
187
|
+
- **Claude Haiku 4.5** overrides this via `extra_params: {anthropic_thinking: {type: enabled, budget_tokens: 2048}}` in `models.yaml` because it still requires the explicit budget.
|
|
188
|
+
- Google Gemini models default to `google_thinking_config={"include_thoughts": True}` when thinking is enabled so their thoughts stream into the UI.
|
|
189
|
+
6. Reasoning-focused OpenAI models (gpt-5, o-series) should be defined under the `openai-responses` provider section so the Responses API (with thinking traces) is used.
|
|
190
|
+
7. `--search` wires up Pydantic AI's `WebSearchTool` only for providers that support it (OpenAI Responses, Anthropic, Gemini, xAI). OpenRouter models automatically switch to their `:online` variant and add the `web` plugin so search works there too; other providers simply ignore the flag.
|
|
191
|
+
8. Rich console has `highlight=False` to prevent auto-styling numbers
|
|
192
|
+
9. User config functions (`load_user_config`, `update_user_config`) live in `config/settings.py`, not a separate file
|
|
193
|
+
10. Prompts loaded from `src/oi/prompts/` directory, not a Python package
|
|
194
|
+
10. Custom exceptions in `exceptions.py` for proper error handling
|
|
195
|
+
11. Conversation/status label text and colors live in `ui/labels.py`, not `constants.py`
|
|
196
|
+
12. Local slash commands are completed from `local_commands.py`; if you add one, update the command registry there
|
|
197
|
+
13. Slash command completion is readline-like `Tab` completion, not a dropdown selector UI
|
|
198
|
+
14. Paste pills (both images and long text) use Unicode PUA sentinel chars in the input buffer; display-only pill expansion via a prompt_toolkit `Processor`. Text pastes expand inline on submit, images become `BinaryContent` parts. Long-text threshold is a fixed `PASTE_LINE_THRESHOLD` in `input_handler.py` (not a function of terminal size — the true failure mode is rendered-rows vs scrollback, which a source-line threshold can only approximate, so the constant is the honest choice). Don't bind `Ctrl+V` — terminals hijack it for paste
|
|
199
|
+
|
|
200
|
+
**Quick Tests:**
|
|
201
|
+
```bash
|
|
202
|
+
uv run oi --help # Smoke test
|
|
203
|
+
uv run python -c "from oi.registry import ModelRegistry; print(list(ModelRegistry().get_available_models().keys()))" # Test model loading
|
|
204
|
+
|
|
205
|
+
# Headless e2e smoke — `--ephemeral` guarantees no chat dir is written or modified.
|
|
206
|
+
# Use a cheap/fast model and `--no-thinking` for deterministic, grep-friendly output.
|
|
207
|
+
uv run oi -p "say only the word PONG" --ephemeral -m haiku --no-thinking
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Do NOT reach for `-c --ephemeral -p "..."` as a casual smoke test — `-c` loads the user's actual latest chat, so even with `--ephemeral` (no save) you still send their full real conversation to the API and then prompt the model with something unrelated. Waste of tokens, confusing for the model. If you really need to exercise the multi-turn path, create an explicit fixture chat first.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
|
|
7
|
+
while pre-1.0, minor version bumps may include breaking changes (CLI flags,
|
|
8
|
+
config/`models.yaml` format, alias names).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-05-24
|
|
13
|
+
|
|
14
|
+
Initial public release.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Rebranded from `llm-cli` to `oi`: command, Python package, config/data
|
|
19
|
+
directories, and PyPI distribution name (`oi-chat`).
|
|
20
|
+
- Upgraded `pydantic-ai` to 1.100 and migrated from `builtin_tools` to the
|
|
21
|
+
`native_tools` API.
|
|
22
|
+
- Use the non-deprecated `google` provider prefix (was `google-gla`).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- PyPI packaging metadata (license, classifiers, project URLs).
|
|
27
|
+
- Support for Python 3.10–3.13.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/dansclearov/oi/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/dansclearov/oi/releases/tag/v0.1.0
|
oi_chat-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
oi_chat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dan Sclearov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|