keycall 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.
- keycall-0.1.0/.github/workflows/ci.yml +79 -0
- keycall-0.1.0/.github/workflows/release.yml +130 -0
- keycall-0.1.0/.gitignore +52 -0
- keycall-0.1.0/CHANGELOG.md +124 -0
- keycall-0.1.0/LICENSE +661 -0
- keycall-0.1.0/PKG-INFO +141 -0
- keycall-0.1.0/README.md +104 -0
- keycall-0.1.0/USAGE.md +278 -0
- keycall-0.1.0/keycall-test-keys.example.toml +47 -0
- keycall-0.1.0/keycall-test-keys.example.txt +21 -0
- keycall-0.1.0/pyproject.toml +67 -0
- keycall-0.1.0/src/keycall/__init__.py +63 -0
- keycall-0.1.0/src/keycall/_cache.py +65 -0
- keycall-0.1.0/src/keycall/_capabilities.py +32 -0
- keycall-0.1.0/src/keycall/_catalog/catalog.json +82 -0
- keycall-0.1.0/src/keycall/_classify.py +59 -0
- keycall-0.1.0/src/keycall/_cli.py +197 -0
- keycall-0.1.0/src/keycall/_client.py +496 -0
- keycall-0.1.0/src/keycall/_credential.py +65 -0
- keycall-0.1.0/src/keycall/_dnsguard.py +135 -0
- keycall-0.1.0/src/keycall/_enums.py +36 -0
- keycall-0.1.0/src/keycall/_errors.py +68 -0
- keycall-0.1.0/src/keycall/_registry.py +207 -0
- keycall-0.1.0/src/keycall/_sanitize.py +46 -0
- keycall-0.1.0/src/keycall/_sources.py +235 -0
- keycall-0.1.0/src/keycall/_tracing.py +124 -0
- keycall-0.1.0/src/keycall/_transport.py +392 -0
- keycall-0.1.0/src/keycall/_types.py +279 -0
- keycall-0.1.0/src/keycall/adapters/__init__.py +33 -0
- keycall-0.1.0/src/keycall/adapters/_anthropic.py +169 -0
- keycall-0.1.0/src/keycall/adapters/_base.py +123 -0
- keycall-0.1.0/src/keycall/adapters/_gemini.py +240 -0
- keycall-0.1.0/src/keycall/adapters/_openai.py +133 -0
- keycall-0.1.0/src/keycall/adapters/_openai_compat.py +127 -0
- keycall-0.1.0/src/keycall/adapters/_perplexity.py +78 -0
- keycall-0.1.0/src/keycall/py.typed +0 -0
- keycall-0.1.0/tests/conftest.py +19 -0
- keycall-0.1.0/tests/test_adapters.py +272 -0
- keycall-0.1.0/tests/test_classify.py +37 -0
- keycall-0.1.0/tests/test_cli.py +94 -0
- keycall-0.1.0/tests/test_client.py +207 -0
- keycall-0.1.0/tests/test_credential.py +62 -0
- keycall-0.1.0/tests/test_hardening.py +414 -0
- keycall-0.1.0/tests/test_registry.py +80 -0
- keycall-0.1.0/tests/test_sources.py +145 -0
- keycall-0.1.0/tests/test_tracing.py +114 -0
- keycall-0.1.0/tests/test_transport.py +136 -0
- keycall-0.1.0/tests/test_types.py +96 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install
|
|
27
|
+
run: pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: ruff check src tests
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
# No provider secrets: the suite runs against recorded fixtures only.
|
|
34
|
+
run: pytest -q
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
|
|
45
|
+
- name: Build
|
|
46
|
+
run: |
|
|
47
|
+
pip install build twine
|
|
48
|
+
python -m build
|
|
49
|
+
|
|
50
|
+
- name: Check metadata
|
|
51
|
+
run: twine check dist/*
|
|
52
|
+
|
|
53
|
+
- name: Verify package data ships
|
|
54
|
+
# catalog.json and py.typed are easy to lose to a packaging change
|
|
55
|
+
# and only fail at runtime, so assert them here.
|
|
56
|
+
run: |
|
|
57
|
+
python - <<'EOF'
|
|
58
|
+
import glob, zipfile, sys
|
|
59
|
+
wheel = sorted(glob.glob("dist/*.whl"))[-1]
|
|
60
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
61
|
+
missing = [
|
|
62
|
+
f for f in ("keycall/_catalog/catalog.json", "keycall/py.typed")
|
|
63
|
+
if f not in names
|
|
64
|
+
]
|
|
65
|
+
if missing:
|
|
66
|
+
sys.exit(f"missing from wheel: {missing}")
|
|
67
|
+
print(f"{wheel}: package data present")
|
|
68
|
+
EOF
|
|
69
|
+
|
|
70
|
+
- name: Install from wheel and smoke test
|
|
71
|
+
run: |
|
|
72
|
+
python -m venv /tmp/smoke
|
|
73
|
+
/tmp/smoke/bin/pip install dist/*.whl
|
|
74
|
+
/tmp/smoke/bin/python -c "
|
|
75
|
+
import keycall
|
|
76
|
+
from keycall._registry import resolve_provider, catalog_version
|
|
77
|
+
assert resolve_provider('anthropic').base_url.startswith('https://')
|
|
78
|
+
print('keycall', keycall.__version__, 'catalog', catalog_version())
|
|
79
|
+
"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing is tag-driven: push a v* tag, and this builds, verifies,
|
|
4
|
+
# publishes to PyPI, and cuts the GitHub release. Nothing publishes from a
|
|
5
|
+
# developer machine.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
inputs:
|
|
11
|
+
test_pypi:
|
|
12
|
+
description: "Publish to TestPyPI instead of PyPI"
|
|
13
|
+
type: boolean
|
|
14
|
+
default: true
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
|
|
29
|
+
- name: Install tooling
|
|
30
|
+
run: pip install build twine
|
|
31
|
+
|
|
32
|
+
- name: Verify tag matches package version
|
|
33
|
+
# A tag that disagrees with pyproject would publish the wrong version
|
|
34
|
+
# under the right name, which cannot be undone on PyPI.
|
|
35
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
36
|
+
run: |
|
|
37
|
+
python - <<'EOF'
|
|
38
|
+
import os, sys, tomllib
|
|
39
|
+
with open("pyproject.toml", "rb") as f:
|
|
40
|
+
version = tomllib.load(f)["project"]["version"]
|
|
41
|
+
tag = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
42
|
+
if tag != version:
|
|
43
|
+
sys.exit(f"tag {tag!r} != pyproject version {version!r}")
|
|
44
|
+
print(f"version {version} matches tag")
|
|
45
|
+
EOF
|
|
46
|
+
|
|
47
|
+
- name: Run tests
|
|
48
|
+
run: |
|
|
49
|
+
pip install -e ".[dev]"
|
|
50
|
+
pytest -q
|
|
51
|
+
|
|
52
|
+
- name: Build
|
|
53
|
+
run: python -m build
|
|
54
|
+
|
|
55
|
+
- name: Check metadata
|
|
56
|
+
run: twine check dist/*
|
|
57
|
+
|
|
58
|
+
- name: Verify package data ships
|
|
59
|
+
run: |
|
|
60
|
+
python - <<'EOF'
|
|
61
|
+
import glob, zipfile, sys
|
|
62
|
+
wheel = sorted(glob.glob("dist/*.whl"))[-1]
|
|
63
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
64
|
+
missing = [
|
|
65
|
+
f for f in ("keycall/_catalog/catalog.json", "keycall/py.typed")
|
|
66
|
+
if f not in names
|
|
67
|
+
]
|
|
68
|
+
if missing:
|
|
69
|
+
sys.exit(f"missing from wheel: {missing}")
|
|
70
|
+
EOF
|
|
71
|
+
|
|
72
|
+
- uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
76
|
+
|
|
77
|
+
publish-testpypi:
|
|
78
|
+
needs: build
|
|
79
|
+
if: github.event_name == 'workflow_dispatch' && inputs.test_pypi
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
environment: testpypi
|
|
82
|
+
permissions:
|
|
83
|
+
id-token: write # OIDC for trusted publishing; no API token stored
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/download-artifact@v4
|
|
86
|
+
with:
|
|
87
|
+
name: dist
|
|
88
|
+
path: dist/
|
|
89
|
+
|
|
90
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
91
|
+
with:
|
|
92
|
+
repository-url: https://test.pypi.org/legacy/
|
|
93
|
+
|
|
94
|
+
publish-pypi:
|
|
95
|
+
needs: build
|
|
96
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
environment: pypi
|
|
99
|
+
permissions:
|
|
100
|
+
id-token: write # OIDC for trusted publishing; no API token stored
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/download-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: dist
|
|
105
|
+
path: dist/
|
|
106
|
+
|
|
107
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
108
|
+
|
|
109
|
+
github-release:
|
|
110
|
+
needs: publish-pypi
|
|
111
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
112
|
+
runs-on: ubuntu-latest
|
|
113
|
+
permissions:
|
|
114
|
+
contents: write # needed to create the release
|
|
115
|
+
steps:
|
|
116
|
+
- uses: actions/checkout@v4
|
|
117
|
+
|
|
118
|
+
- uses: actions/download-artifact@v4
|
|
119
|
+
with:
|
|
120
|
+
name: dist
|
|
121
|
+
path: dist/
|
|
122
|
+
|
|
123
|
+
- name: Create GitHub release
|
|
124
|
+
env:
|
|
125
|
+
GH_TOKEN: ${{ github.token }}
|
|
126
|
+
run: |
|
|
127
|
+
gh release create "$GITHUB_REF_NAME" \
|
|
128
|
+
--title "$GITHUB_REF_NAME" \
|
|
129
|
+
--notes "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)." \
|
|
130
|
+
dist/*
|
keycall-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Internal design docs (PRD, naming decisions, research) — not part of the public package.
|
|
2
|
+
/internal/
|
|
3
|
+
|
|
4
|
+
# macOS and system files.
|
|
5
|
+
.DS_Store
|
|
6
|
+
.AppleDouble
|
|
7
|
+
.LSOverride
|
|
8
|
+
._*
|
|
9
|
+
__MACOSX/
|
|
10
|
+
Thumbs.db
|
|
11
|
+
Desktop.ini
|
|
12
|
+
|
|
13
|
+
# Local environment and tool state.
|
|
14
|
+
.env
|
|
15
|
+
.env.*
|
|
16
|
+
.claude/
|
|
17
|
+
.codex/
|
|
18
|
+
|
|
19
|
+
# Live-test credential files (the .example templates stay tracked).
|
|
20
|
+
keycall-test-keys.*
|
|
21
|
+
!keycall-test-keys.example.*
|
|
22
|
+
|
|
23
|
+
# Internal documentation (top-level project docs stay tracked).
|
|
24
|
+
*.md
|
|
25
|
+
!README.md
|
|
26
|
+
!USAGE.md
|
|
27
|
+
!CHANGELOG.md
|
|
28
|
+
!CONTRIBUTING.md
|
|
29
|
+
|
|
30
|
+
# Python build/runtime artifacts.
|
|
31
|
+
__pycache__/
|
|
32
|
+
*.py[cod]
|
|
33
|
+
*.egg-info/
|
|
34
|
+
dist/
|
|
35
|
+
build/
|
|
36
|
+
.venv/
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.ruff_cache/
|
|
40
|
+
.coverage
|
|
41
|
+
htmlcov/
|
|
42
|
+
|
|
43
|
+
# Build/package artifacts and logs.
|
|
44
|
+
*.zip
|
|
45
|
+
*.tar
|
|
46
|
+
*.tar.gz
|
|
47
|
+
*.tgz
|
|
48
|
+
*.log
|
|
49
|
+
|
|
50
|
+
# Editor/project metadata.
|
|
51
|
+
.idea/
|
|
52
|
+
.vscode/
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to KeyCall are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
Nothing yet.
|
|
11
|
+
|
|
12
|
+
## [0.1.0] — 2026-08-05
|
|
13
|
+
|
|
14
|
+
First release. Key validation, model discovery and filtering, and text
|
|
15
|
+
generation, live-verified against every supported provider.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
**Clients**
|
|
20
|
+
|
|
21
|
+
- `KeyCall` and `AsyncKeyCall` with identical surfaces; only awaiting differs.
|
|
22
|
+
- Provider and credential bound once at construction as immutable client
|
|
23
|
+
identity: no setters, no per-call override, no public `api_key` property.
|
|
24
|
+
- Context-manager support; `close()` releases the credential and HTTP client.
|
|
25
|
+
- Configurable `connect_timeout`, `read_timeout`, `max_response_bytes`,
|
|
26
|
+
`trust_env`, `allow_insecure_localhost`, and `allow_private_network`.
|
|
27
|
+
|
|
28
|
+
**Model discovery**
|
|
29
|
+
|
|
30
|
+
- `list_models(categories=..., refresh=...)` returning a `ModelDiscovery`
|
|
31
|
+
envelope with `models`, `fetched_at`, `from_cache`, `catalog_version`, and
|
|
32
|
+
`warnings`.
|
|
33
|
+
- Eight-member `ModelCategory` taxonomy; text generation is the default filter.
|
|
34
|
+
- Conservative classification: explicit provider metadata first, then
|
|
35
|
+
maintained identifier rules. Unclassifiable models resolve to `UNKNOWN` and
|
|
36
|
+
never enter the default text picker.
|
|
37
|
+
- Process-local availability cache with a 5-minute TTL, keyed by an HMAC
|
|
38
|
+
fingerprint of the credential rather than the credential itself.
|
|
39
|
+
|
|
40
|
+
**Text generation**
|
|
41
|
+
|
|
42
|
+
- `invoke(TextGenerationRequest)` as the low-level primitive and
|
|
43
|
+
`generate_text(...)` as the convenience path.
|
|
44
|
+
- Normalized `InvocationResult` with typed output parts, `usage`,
|
|
45
|
+
`round_trip_duration_ms`, `provider_request_id`, `finish_reason`, and
|
|
46
|
+
`warnings`.
|
|
47
|
+
- `temperature` and `top_p`, validated at construction and omitted from the
|
|
48
|
+
wire body when unset. Models with maintained evidence that they reject
|
|
49
|
+
sampling parameters (OpenAI o-series and gpt-5; Anthropic Opus 4.7+,
|
|
50
|
+
Opus 5+, Sonnet 5+) fail with `MODEL_NOT_SUITABLE` before any network call.
|
|
51
|
+
- Usage fields distinguish "provider reported zero" from "provider did not
|
|
52
|
+
report", which stays `None` and is never fabricated.
|
|
53
|
+
|
|
54
|
+
**Providers**
|
|
55
|
+
|
|
56
|
+
- OpenAI (Responses API), Anthropic (Messages), Google Gemini
|
|
57
|
+
(`generateContent`), DeepSeek, Perplexity (Sonar), Moonshot/Kimi.
|
|
58
|
+
- Explicit OpenAI-compatible custom targets via `protocol` plus `base_url`.
|
|
59
|
+
- Provider identity and wire protocol kept as separate concepts; named
|
|
60
|
+
providers may override their protocol's default adapter.
|
|
61
|
+
|
|
62
|
+
**Errors**
|
|
63
|
+
|
|
64
|
+
- Single `KeyCallError` with a typed `ErrorCode` discriminator, plus
|
|
65
|
+
`retryable`, `status_code`, `provider_request_id`, and `retry_after`.
|
|
66
|
+
- Twelve normalized codes spanning credential, model, provider, transport, and
|
|
67
|
+
setup failures.
|
|
68
|
+
|
|
69
|
+
**Security**
|
|
70
|
+
|
|
71
|
+
- Credentials wrapped in a redacting type at the single public entry point;
|
|
72
|
+
excluded from reprs, formatting, exceptions, logs, traces, copies, and
|
|
73
|
+
pickles, with canary tests asserting absence.
|
|
74
|
+
- `Credential.reveal()` called from exactly one place, the transport layer's
|
|
75
|
+
header builder.
|
|
76
|
+
- Provider-originated error text scrubbed for credential values, credential
|
|
77
|
+
patterns, and control characters, then length-bounded, before reaching any
|
|
78
|
+
result, exception, log, or trace.
|
|
79
|
+
- Redirects refused rather than followed while carrying a credential.
|
|
80
|
+
- Response bodies read incrementally against a 10 MB cap.
|
|
81
|
+
- SSRF guard rejecting literal private, loopback, link-local, and reserved IP
|
|
82
|
+
targets unless explicitly opted in.
|
|
83
|
+
- DNS-rebinding guard for custom endpoints: resolves once, rejects if any
|
|
84
|
+
resolved address is private, then connects to the validated address while
|
|
85
|
+
preserving the original hostname for TLS SNI and `Host`.
|
|
86
|
+
- HTTPS required for custom endpoints; plain HTTP only for localhost behind an
|
|
87
|
+
explicit flag.
|
|
88
|
+
|
|
89
|
+
**Reliability**
|
|
90
|
+
|
|
91
|
+
- Operation-aware retries: bounded retries with backoff and `Retry-After`
|
|
92
|
+
support for model listing; none for generation, since no supported provider
|
|
93
|
+
documents generation idempotency.
|
|
94
|
+
- Explicit connect and read timeouts on every request.
|
|
95
|
+
|
|
96
|
+
**CLI**
|
|
97
|
+
|
|
98
|
+
- `keycall verify` for live credential verification from TXT, JSON, or TOML
|
|
99
|
+
files, an explicit `env:VAR_NAME` reference, or an interactive prompt.
|
|
100
|
+
- `--generate` makes one bounded call per target, walking filtered models in
|
|
101
|
+
provider order and reporting every attempt (`--attempts`, default 8) so
|
|
102
|
+
provider drift stays visible.
|
|
103
|
+
- `--strict-credentials` promotes credential-file warnings to errors.
|
|
104
|
+
- Credential files are never modified or deleted; keys never appear in output.
|
|
105
|
+
|
|
106
|
+
**Observability**
|
|
107
|
+
|
|
108
|
+
- Optional TraceAct integration emitting `keycall.list_models` and
|
|
109
|
+
`keycall.text_generation` spans with safe fields only. Silent when TraceAct
|
|
110
|
+
is absent or the host has not configured it; disabled with one warning on an
|
|
111
|
+
incompatible version.
|
|
112
|
+
|
|
113
|
+
### Known limitations
|
|
114
|
+
|
|
115
|
+
- Streaming, tool calling, structured output, and non-text modalities are not
|
|
116
|
+
implemented.
|
|
117
|
+
- Gemini's list endpoint advertises models an account cannot invoke and exposes
|
|
118
|
+
no lifecycle field, so they cannot be pre-filtered.
|
|
119
|
+
- Perplexity's Sonar models are not API-discoverable and are maintained in the
|
|
120
|
+
bundled catalog.
|
|
121
|
+
- The provider catalog ships inside the package and updates only on release.
|
|
122
|
+
|
|
123
|
+
[Unreleased]: https://github.com/shehuphd/keycall/compare/v0.1.0...HEAD
|
|
124
|
+
[0.1.0]: https://github.com/shehuphd/keycall/releases/tag/v0.1.0
|