From 2d8c4777f04cf303ffeec9b514856b9ae79aa660 Mon Sep 17 00:00:00 2001 From: Daniel Yudelevich Date: Tue, 20 Oct 2026 10:56:40 -0700 Subject: [PATCH 1/5] feat(llm-providers): region, account and project settings The API now reads provider settings from an LLM integration's model_name after "#" (discolike-platform!1041), so a Bedrock key scoped to one AWS region works and Snowflake and watsonx become usable. Expose the accepted regions on DiscogenModels and give the CLI flags that build the suffix, so nobody has to hand-encode it. --- CHANGELOG.md | 4 ++ .../src/discolike_cli/providers.py | 53 ++++++++++++++++--- .../discolike-cli/tests/test_discogen_cli.py | 5 +- .../discolike-cli/tests/test_providers_cli.py | 35 ++++++++++++ .../src/discolike/resources/discogen.py | 4 ++ packages/discolike/tests/test_discogen.py | 4 +- 6 files changed, 97 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98473af..355cde2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- SDK/CLI: an LLM integration's `model_name` can carry provider settings after `#`, as URL query parameters: `region` for `bedrock` and `bedrock_mantle` (any AWS region, optional), `account` for `snowflake` (account identifier, with a programmatic access token as `api_key`) and `region` plus `project` for `watsonx`, e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1`. Snowflake and watsonx become usable as providers. An unknown setting or region is a 400 (`ValidationError`). `DiscogenModels.provider_regions` lists the accepted regions per provider. The CLI's `llm-providers create`, `update` and `test-connection` gain `--region`, `--account` and `--project`, which build the suffix for you, and `discogen models` output gains a `provider_regions` key. It needs an API release that knows the settings; older SDKs can already send the suffix in `model_name`. + ## 0.6.1 (2026-10-19) - SDK/CLI: discover's server-side default for `min_digital_footprint` drops from 50 to 25 when the field is omitted, so lists include more real companies with a thin web presence. Pass `min_digital_footprint=50` (CLI `--min-digital-footprint 50`) to keep the old floor. No SDK change; older versions get the new default too. diff --git a/packages/discolike-cli/src/discolike_cli/providers.py b/packages/discolike-cli/src/discolike_cli/providers.py index 93d8701..5f7ccd3 100644 --- a/packages/discolike-cli/src/discolike_cli/providers.py +++ b/packages/discolike-cli/src/discolike_cli/providers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from urllib.parse import urlencode + import typer from discolike.requests import LLMProviderCreateRequest @@ -15,6 +17,20 @@ search_providers_app = typer.Typer(help="Manage BYOK web search provider integrations (Tavily, Serper, etc.).") llm_providers_app = typer.Typer(help="Manage BYOK LLM provider integrations (OpenAI, Anthropic, custom endpoints).") +LLM_PROVIDER_HELP = "LLM provider key, e.g. openai, anthropic, bedrock, snowflake, watsonx, or custom." +REGION_HELP = ( + "Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) " + "for watsonx. `discogen models` lists them under provider_regions." +) +ACCOUNT_HELP = "Snowflake account identifier, e.g. orgname-accountname (snowflake only)." +PROJECT_HELP = "watsonx project ID (watsonx only)." + + +def _with_model_settings(model_name: str, *, region: str | None, account: str | None, project: str | None) -> str: + """The API reads provider settings from model_name after "#", e.g. "#region=us-east-1".""" + settings = {key: value for key, value in (("region", region), ("account", account), ("project", project)) if value} + return f"{model_name}#{urlencode(settings)}" if settings else model_name + @search_providers_app.command("list") @handle_errors @@ -149,10 +165,13 @@ def llm_list_command( def llm_create_command( ctx: typer.Context, name: str = typer.Option(..., "--name", help="User-friendly name for the integration."), - provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."), + provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP), api_key: str = typer.Option(..., "--api-key", help="API key for the provider."), model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."), base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."), + region: str | None = typer.Option(None, "--region", help=REGION_HELP), + account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP), + project: str | None = typer.Option(None, "--project", help=PROJECT_HELP), ) -> None: """Create an LLM provider integration.""" from discolike_cli.main import get_client @@ -160,7 +179,12 @@ def llm_create_command( request = build_request( LLMProviderCreateRequest, _merge_params( - None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url + None, + integration_name=name, + provider=provider, + api_key=api_key, + model_name=_with_model_settings(model_name, region=region, account=account, project=project), + base_url=base_url, ), ) emit(get_client(ctx).llm_providers.create(request)) @@ -185,10 +209,13 @@ def llm_update_command( ctx: typer.Context, integration_id: str = typer.Argument(..., help="ID of the LLM provider integration to update."), name: str = typer.Option(..., "--name", help="User-friendly name for the integration."), - provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."), + provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP), model_name: str = typer.Option(..., "--model-name", help="Default model name for this integration."), api_key: str | None = typer.Option(None, "--api-key", help="New API key (omit to keep the stored key)."), base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."), + region: str | None = typer.Option(None, "--region", help=REGION_HELP), + account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP), + project: str | None = typer.Option(None, "--project", help=PROJECT_HELP), ) -> None: """Update an LLM provider integration.""" from discolike_cli.main import get_client @@ -196,7 +223,13 @@ def llm_update_command( request = build_request( LLMProviderUpdateRequest, { - **_merge_params(None, integration_name=name, provider=provider, model_name=model_name, base_url=base_url), + **_merge_params( + None, + integration_name=name, + provider=provider, + model_name=_with_model_settings(model_name, region=region, account=account, project=project), + base_url=base_url, + ), "api_key": api_key, }, ) @@ -233,10 +266,13 @@ def llm_set_default_command( def llm_test_connection_command( ctx: typer.Context, name: str = typer.Option("cli-test", "--name", help="Name to label the test configuration."), - provider: str = typer.Option(..., "--provider", help="LLM provider name: openai, anthropic, or custom."), + provider: str = typer.Option(..., "--provider", help=LLM_PROVIDER_HELP), api_key: str = typer.Option(..., "--api-key", help="API key for the provider."), model_name: str = typer.Option(..., "--model-name", help="Model name to test against."), base_url: str | None = typer.Option(None, "--base-url", help="Endpoint URL (required for the custom provider)."), + region: str | None = typer.Option(None, "--region", help=REGION_HELP), + account: str | None = typer.Option(None, "--account", help=ACCOUNT_HELP), + project: str | None = typer.Option(None, "--project", help=PROJECT_HELP), ) -> None: """Test a provider configuration before saving it.""" from discolike_cli.main import get_client @@ -244,7 +280,12 @@ def llm_test_connection_command( request = build_request( LLMProviderCreateRequest, _merge_params( - None, integration_name=name, provider=provider, api_key=api_key, model_name=model_name, base_url=base_url + None, + integration_name=name, + provider=provider, + api_key=api_key, + model_name=_with_model_settings(model_name, region=region, account=account, project=project), + base_url=base_url, ), ) emit(get_client(ctx).llm_providers.test_connection(request)) diff --git a/packages/discolike-cli/tests/test_discogen_cli.py b/packages/discolike-cli/tests/test_discogen_cli.py index 7095e79..957339a 100644 --- a/packages/discolike-cli/tests/test_discogen_cli.py +++ b/packages/discolike-cli/tests/test_discogen_cli.py @@ -314,7 +314,10 @@ def handler(request: httpx2.Request) -> httpx2.Response: install_build_client(handler) result = runner.invoke(app, ["discogen", "models"]) assert result.exit_code == 0, result.output - assert json.loads(result.stdout) == {"models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}} + assert json.loads(result.stdout) == { + "models": {"openai": [{"name": "gpt-5.4", "supports_web_search": False}]}, + "provider_regions": {}, + } def test_discogen_status_default_family_discogen(install_build_client: Callable[[Handler], None]) -> None: diff --git a/packages/discolike-cli/tests/test_providers_cli.py b/packages/discolike-cli/tests/test_providers_cli.py index b365d5a..c694143 100644 --- a/packages/discolike-cli/tests/test_providers_cli.py +++ b/packages/discolike-cli/tests/test_providers_cli.py @@ -138,6 +138,41 @@ def handler(request: httpx2.Request) -> httpx2.Response: } +def test_llm_providers_create_appends_provider_settings_to_model_name( + install_build_client: Callable[[Handler], None], +) -> None: + captured: dict[str, dict[str, str]] = {} + + def handler(request: httpx2.Request) -> httpx2.Response: + captured["body"] = json.loads(request.content) + return httpx2.Response(200, json={"message": "created", "integration_id": "llm1"}) + + install_build_client(handler) + result = runner.invoke( + app, + [ + "llm-providers", + "create", + "--name", + "watsonx", + "--provider", + "watsonx", + "--api-key", + "ibm-key", + "--model-name", + "watsonx/ibm/granite-3-8b-instruct", + "--region", + "eu-de", + "--project", + "11111111-2222-3333-4444-555555555555", + ], + ) + assert result.exit_code == 0, result.output + assert captured["body"]["model_name"] == ( + "watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=11111111-2222-3333-4444-555555555555" + ) + + def test_llm_providers_update_keeps_null_api_key(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {} diff --git a/packages/discolike/src/discolike/resources/discogen.py b/packages/discolike/src/discolike/resources/discogen.py index d6719bd..ce0e455 100644 --- a/packages/discolike/src/discolike/resources/discogen.py +++ b/packages/discolike/src/discolike/resources/discogen.py @@ -46,6 +46,10 @@ class DiscogenModelInfo(DiscolikeModel): class DiscogenModels(DiscolikeModel): models: dict[str, list[DiscogenModelInfo]] = pydantic.Field(default_factory=dict) + provider_regions: dict[str, list[str]] = pydantic.Field( + default_factory=dict, + description='Regions each regional provider accepts; pick one with "#region=" in model_name.', + ) class DiscogenResource(SyncAPIResource): diff --git a/packages/discolike/tests/test_discogen.py b/packages/discolike/tests/test_discogen.py index 20423a7..cba36eb 100644 --- a/packages/discolike/tests/test_discogen.py +++ b/packages/discolike/tests/test_discogen.py @@ -140,7 +140,8 @@ def handler(request: httpx2.Request) -> httpx2.Response: "models": { "xai": [{"name": "grok-4", "supports_web_search": True}], "openai": [{"name": "gpt-5.4", "supports_web_search": False}], - } + }, + "provider_regions": {"bedrock": ["us-east-1", "us-west-2"]}, }, ) @@ -152,6 +153,7 @@ def handler(request: httpx2.Request) -> httpx2.Response: assert result.models["xai"][0].name == "grok-4" assert result.models["xai"][0].supports_web_search is True assert result.models["openai"][0].name == "gpt-5.4" + assert result.provider_regions == {"bedrock": ["us-east-1", "us-west-2"]} def test_job_reattaches_without_http_call(make_client: ClientFactory) -> None: From 6a893b43dd0bd95f36dc20f5a46d025c1c6b5969 Mon Sep 17 00:00:00 2001 From: Daniel Yudelevich Date: Wed, 21 Oct 2026 12:05:25 -0700 Subject: [PATCH 2/5] Release 0.6.2: Python 3.15 support CPython 3.15.0 is final, so 3.15 moves from the non-blocking prerelease job into the test matrix, the highest-resolution job runs on it, and both packages get the classifier. The full SDK and CLI suites, ty, and the built wheels at highest and lowest-direct resolution pass on 3.15.0 locally. Also dates the changelog section for the LLM provider region, account and project settings. --- .github/workflows/ci.yml | 18 ++---------------- CHANGELOG.md | 3 ++- CONTRIBUTING.md | 2 +- packages/discolike-cli/pyproject.toml | 5 +++-- packages/discolike/pyproject.toml | 3 ++- packages/discolike/src/discolike/_version.py | 2 +- packages/discolike/tests/test_package.py | 2 +- uv.lock | 2 +- 8 files changed, 13 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cf64f0..73a73ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11', '3.12', '3.13', '3.14'] + python-version: ['3.11', '3.12', '3.13', '3.14', '3.15'] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 @@ -34,7 +34,7 @@ jobs: matrix: include: - resolution: highest - python-version: '3.14' + python-version: '3.15' - resolution: lowest-direct python-version: '3.11' steps: @@ -47,17 +47,3 @@ jobs: - run: uv run --no-sync pytest packages/discolike-cli/tests -q - run: uv build --all-packages -o dist - run: uv run --no-project --isolated --resolution ${{ matrix.resolution }} --find-links dist --with dist/discolike_cli-*.whl discolike --version - - # 3.15 is still prerelease; non-blocking until 3.15.0 final, when it moves - # into the matrix above and gets a classifier. - test-prerelease: - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - with: - python-version: '3.15' - - run: uv sync --all-packages - - run: uv run pytest packages/discolike/tests -q - - run: uv run pytest packages/discolike-cli/tests -q diff --git a/CHANGELOG.md b/CHANGELOG.md index 355cde2..2050868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## Unreleased +## 0.6.2 (2026-10-21) +- SDK/CLI: Python 3.15 is supported and tested in CI. - SDK/CLI: an LLM integration's `model_name` can carry provider settings after `#`, as URL query parameters: `region` for `bedrock` and `bedrock_mantle` (any AWS region, optional), `account` for `snowflake` (account identifier, with a programmatic access token as `api_key`) and `region` plus `project` for `watsonx`, e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1`. Snowflake and watsonx become usable as providers. An unknown setting or region is a 400 (`ValidationError`). `DiscogenModels.provider_regions` lists the accepted regions per provider. The CLI's `llm-providers create`, `update` and `test-connection` gain `--region`, `--account` and `--project`, which build the suffix for you, and `discogen models` output gains a `provider_regions` key. It needs an API release that knows the settings; older SDKs can already send the suffix in `model_name`. ## 0.6.1 (2026-10-19) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eae594e..4afdc31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ uv run pytest packages/discolike/tests -q uv run pytest packages/discolike-cli/tests -q ``` -CI runs the same checks across Python 3.11–3.14. +CI runs the same checks across Python 3.11–3.15. ## Branches diff --git a/packages/discolike-cli/pyproject.toml b/packages/discolike-cli/pyproject.toml index c25eae5..23b9182 100644 --- a/packages/discolike-cli/pyproject.toml +++ b/packages/discolike-cli/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "discolike-cli" -version = "0.6.1" +version = "0.6.2" description = "Official CLI for the DiscoLike API" readme = "README.md" license = "MIT" @@ -12,7 +12,7 @@ license-files = ["LICENSE"] requires-python = ">=3.11" authors = [{ name = "DiscoLike", email = "support@discolike.com" }] dependencies = [ - "discolike==0.6.1", + "discolike==0.6.2", "typer>=0.26.1,<0.28", "rich>=13.0", ] @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: 3 :: Only", "Operating System :: OS Independent", "Environment :: Console", diff --git a/packages/discolike/pyproject.toml b/packages/discolike/pyproject.toml index 507ad99..0223fa3 100644 --- a/packages/discolike/pyproject.toml +++ b/packages/discolike/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: 3 :: Only", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", @@ -33,7 +34,7 @@ classifiers = [ ] [project.optional-dependencies] -cli = ["discolike-cli==0.6.1"] +cli = ["discolike-cli==0.6.2"] [project.urls] Homepage = "https://www.discolike.com" diff --git a/packages/discolike/src/discolike/_version.py b/packages/discolike/src/discolike/_version.py index 43c4ab0..22049ab 100644 --- a/packages/discolike/src/discolike/_version.py +++ b/packages/discolike/src/discolike/_version.py @@ -1 +1 @@ -__version__ = "0.6.1" +__version__ = "0.6.2" diff --git a/packages/discolike/tests/test_package.py b/packages/discolike/tests/test_package.py index afc8344..9f746fc 100644 --- a/packages/discolike/tests/test_package.py +++ b/packages/discolike/tests/test_package.py @@ -2,4 +2,4 @@ def test_version() -> None: - assert discolike.__version__ == "0.6.1" + assert discolike.__version__ == "0.6.2" diff --git a/uv.lock b/uv.lock index 38b08de..50ba2dd 100644 --- a/uv.lock +++ b/uv.lock @@ -343,7 +343,7 @@ dev = [ [[package]] name = "discolike-cli" -version = "0.6.1" +version = "0.6.2" source = { editable = "packages/discolike-cli" } dependencies = [ { name = "discolike" }, From d87393a659c75f72e2e6e6ece9b7f9e053896d81 Mon Sep 17 00:00:00 2001 From: Daniel Yudelevich Date: Wed, 21 Oct 2026 13:38:03 -0700 Subject: [PATCH 3/5] chore: regenerate request models from the dev spec The platform added linkedin to contact bulk match and reordered the LLM provider request fields, so gen_requests --check against dev went red on the 0.6.2 release PR. --- CHANGELOG.md | 2 ++ .../src/discolike/_generated/requests.py | 25 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2050868..de9e3a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - SDK/CLI: Python 3.15 is supported and tested in CI. - SDK/CLI: an LLM integration's `model_name` can carry provider settings after `#`, as URL query parameters: `region` for `bedrock` and `bedrock_mantle` (any AWS region, optional), `account` for `snowflake` (account identifier, with a programmatic access token as `api_key`) and `region` plus `project` for `watsonx`, e.g. `us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1`. Snowflake and watsonx become usable as providers. An unknown setting or region is a 400 (`ValidationError`). `DiscogenModels.provider_regions` lists the accepted regions per provider. The CLI's `llm-providers create`, `update` and `test-connection` gain `--region`, `--account` and `--project`, which build the suffix for you, and `discogen models` output gains a `provider_regions` key. It needs an API release that knows the settings; older SDKs can already send the suffix in `model_name`. +- SDK: `BulkContactMatchQueryItem` gains `linkedin`, a LinkedIn profile URL or username matched exactly before `email`, then name and company, so a list of profile URLs alone returns the matching contacts. It needs an API release that knows the field. +- SDK (note for maintainers): `discolike/_generated/requests.py` is regenerated from the dev spec, which adds `linkedin` and moves `api_key` after `base_url` on the LLM provider request models (keyword arguments, so no caller change). ## 0.6.1 (2026-10-19) diff --git a/packages/discolike/src/discolike/_generated/requests.py b/packages/discolike/src/discolike/_generated/requests.py index a519c7d..3749f6b 100644 --- a/packages/discolike/src/discolike/_generated/requests.py +++ b/packages/discolike/src/discolike/_generated/requests.py @@ -2905,7 +2905,6 @@ class LLMProviderCreateRequest(DiscolikeRequest): title="Provider", ), ] - api_key: Annotated[str, Field(description="API key for the provider", title="Api Key")] model_name: Annotated[ str, Field(description="Default model name for this integration", title="Model Name"), @@ -2917,6 +2916,7 @@ class LLMProviderCreateRequest(DiscolikeRequest): title="Base Url", ), ] = None + api_key: Annotated[str, Field(description="API key for the provider", title="Api Key")] class LLMProviderUpdateRequest(DiscolikeRequest): @@ -2934,13 +2934,6 @@ class LLMProviderUpdateRequest(DiscolikeRequest): title="Provider", ), ] - api_key: Annotated[ - str | None, - Field( - description="API key for the provider (null to keep existing)", - title="Api Key", - ), - ] model_name: Annotated[ str, Field(description="Default model name for this integration", title="Model Name"), @@ -2952,6 +2945,13 @@ class LLMProviderUpdateRequest(DiscolikeRequest): title="Base Url", ), ] = None + api_key: Annotated[ + str | None, + Field( + description="API key for the provider (null to keep existing)", + title="Api Key", + ), + ] class SearchProviderRequest(DiscolikeRequest): @@ -3084,7 +3084,7 @@ class BulkContactMatchQueryItem(DiscolikeRequest): name: Annotated[ str | None, Field( - description="Person name to search for. Required unless email is provided.", + description="Person name to search for. Required unless email or linkedin is provided.", title="Name", ), ] = None @@ -3095,6 +3095,13 @@ class BulkContactMatchQueryItem(DiscolikeRequest): title="Email", ), ] = None + linkedin: Annotated[ + str | None, + Field( + description="LinkedIn profile URL or username for exact lookup, tried before email. When found, email and name/company matching are skipped for this row.", + title="Linkedin", + ), + ] = None company_name: Annotated[ str | None, Field(description="Company name to narrow the search.", title="Company Name"), From fe8b13e938553cb25824cbd5de83b1cdb9d26782 Mon Sep 17 00:00:00 2001 From: Daniel Yudelevich Date: Wed, 21 Oct 2026 13:42:01 -0700 Subject: [PATCH 4/5] fix(examples): use max_wait and coalesce missing address fields The polling kwarg was renamed from timeout to max_wait, which left three examples raising TypeError at the wait call, and a None address field broke enrich_crm_export's dict[str, str] return. --- examples/discover_and_enrich.py | 2 +- examples/enrich_crm_export.py | 6 +++--- examples/find_emails_from_csv.py | 2 +- examples/match_crm_contacts.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/discover_and_enrich.py b/examples/discover_and_enrich.py index 6cdff84..6f6447c 100644 --- a/examples/discover_and_enrich.py +++ b/examples/discover_and_enrich.py @@ -54,7 +54,7 @@ def main() -> None: print(f"\nRunning DiscoGen over {len(domains)} domains: {args.query!r}") job = client.discogen.process(DiscoGenProcessRequest(query=args.query, domains=domains, web_search=True)) - status = job.wait(timeout=args.timeout, on_poll=lambda s: print(f" status={s.status} progress={s.progress}%")) + status = job.wait(max_wait=args.timeout, on_poll=lambda s: print(f" status={s.status} progress={s.progress}%")) print("\nEnriched results:") for row in status.results or []: diff --git a/examples/enrich_crm_export.py b/examples/enrich_crm_export.py index 6322a9f..e30406f 100644 --- a/examples/enrich_crm_export.py +++ b/examples/enrich_crm_export.py @@ -47,9 +47,9 @@ def enrich_row(client: Discolike, *, domain: str) -> dict[str, str]: "dl_name": profile.name or "", "dl_employees": profile.employees or "", "dl_revenue_range": profile.revenue_range or "", - "dl_country": address.country if address else "", - "dl_state": address.state if address else "", - "dl_city": address.city if address else "", + "dl_country": (address.country if address else None) or "", + "dl_state": (address.state if address else None) or "", + "dl_city": (address.city if address else None) or "", "dl_industry_groups": top_keys(profile.industry_groups), "dl_business_model": top_keys(profile.business_model), "dl_description": profile.description or "", diff --git a/examples/find_emails_from_csv.py b/examples/find_emails_from_csv.py index 228ca6b..930e71b 100644 --- a/examples/find_emails_from_csv.py +++ b/examples/find_emails_from_csv.py @@ -68,7 +68,7 @@ def main() -> None: chunk = contacts[start : start + MAX_CONTACTS_PER_BATCH] print(f"Submitting batch of {len(chunk)} contacts ({start + len(chunk)}/{len(contacts)})...") batch = client.email.find_batch(FindEmailBatchRequest.model_validate({"requests": chunk})) - results = batch.results(timeout=args.timeout) + results = batch.results(max_wait=args.timeout) for item in results.results: output = item.result if not isinstance(output, EnumerationOutput): diff --git a/examples/match_crm_contacts.py b/examples/match_crm_contacts.py index 55fa8e4..dd49a2f 100644 --- a/examples/match_crm_contacts.py +++ b/examples/match_crm_contacts.py @@ -208,7 +208,7 @@ def main() -> None: job = client.contacts.bulk_match( BulkContactMatchRequest.model_validate({"queries": queries, "limit": 1}) ) - status = job.wait(timeout=args.timeout) + status = job.wait(max_wait=args.timeout) except Exception as exc: # leave the chunk un-checkpointed so a rerun retries it failed_chunks += 1 print(f" ! {label} failed ({exc}); rows left pending, rerun to retry") From af58f637811e2750a10c0c6cf580f4b23dfd3bfc Mon Sep 17 00:00:00 2001 From: Daniel Yudelevich Date: Sat, 24 Oct 2026 08:15:45 -0700 Subject: [PATCH 5/5] fix(cli): let provider setting flags override a model_name suffix A model_name read back from llm-providers get already carries #region=..., so passing --region on update appended a second suffix the API rejects instead of changing the region. --- .../src/discolike_cli/providers.py | 8 +++-- .../discolike-cli/tests/test_providers_cli.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/discolike-cli/src/discolike_cli/providers.py b/packages/discolike-cli/src/discolike_cli/providers.py index 5f7ccd3..0f9e767 100644 --- a/packages/discolike-cli/src/discolike_cli/providers.py +++ b/packages/discolike-cli/src/discolike_cli/providers.py @@ -1,5 +1,6 @@ from __future__ import annotations +from urllib.parse import parse_qsl from urllib.parse import urlencode import typer @@ -28,8 +29,11 @@ def _with_model_settings(model_name: str, *, region: str | None, account: str | None, project: str | None) -> str: """The API reads provider settings from model_name after "#", e.g. "#region=us-east-1".""" - settings = {key: value for key, value in (("region", region), ("account", account), ("project", project)) if value} - return f"{model_name}#{urlencode(settings)}" if settings else model_name + flags = {key: value for key, value in (("region", region), ("account", account), ("project", project)) if value} + if not flags: + return model_name + base, _, existing = model_name.partition("#") + return f"{base}#{urlencode(dict(parse_qsl(existing)) | flags)}" @search_providers_app.command("list") diff --git a/packages/discolike-cli/tests/test_providers_cli.py b/packages/discolike-cli/tests/test_providers_cli.py index c694143..45fa8e3 100644 --- a/packages/discolike-cli/tests/test_providers_cli.py +++ b/packages/discolike-cli/tests/test_providers_cli.py @@ -173,6 +173,36 @@ def handler(request: httpx2.Request) -> httpx2.Response: ) +def test_llm_providers_update_flags_override_an_existing_settings_suffix( + install_build_client: Callable[[Handler], None], +) -> None: + captured: dict[str, dict[str, str]] = {} + + def handler(request: httpx2.Request) -> httpx2.Response: + captured["body"] = json.loads(request.content) + return httpx2.Response(200, json={"message": "updated"}) + + install_build_client(handler) + result = runner.invoke( + app, + [ + "llm-providers", + "update", + "llm1", + "--name", + "watsonx", + "--provider", + "watsonx", + "--model-name", + "watsonx/ibm/granite-3-8b-instruct#region=us-south&project=p1", + "--region", + "eu-de", + ], + ) + assert result.exit_code == 0, result.output + assert captured["body"]["model_name"] == "watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=p1" + + def test_llm_providers_update_keeps_null_api_key(install_build_client: Callable[[Handler], None]) -> None: captured: dict[str, object] = {}