diff --git a/NEWS.md b/NEWS.md index 07dba40c..71e933fc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # ellmer (development version) +* `chat_portkey()` now requires you to supply a model (#786). +* `chat_portkey(virtual_key)` no longer needs to be supplied; instead Portkey recommends including the virtual key/povider in the `model`.(#786). * `batch_chat()` logs tokens once, on retrieval (#743). * `params()` gains new `reasoning_effort` and `reasoning_tokens` so you can control the amount of effort a model spends on thinking. Initial support is provided for `chat_claude()`, `chat_google_gemini()`, and `chat_openai()` (#720). * `chat_anthropic()` gains new `cache` parameter to control caching. By default it is set to "5m". This should (on average) reduce the cost of your chats.(#584) diff --git a/R/provider-portkey.R b/R/provider-portkey.R index e00c73cf..e76b90a9 100644 --- a/R/provider-portkey.R +++ b/R/provider-portkey.R @@ -3,42 +3,61 @@ #' @description #' [PortkeyAI](https://portkey.ai/docs/product/ai-gateway/universal-api) #' provides an interface (AI Gateway) to connect through its Universal API to a -#' variety of LLMs providers with a single endpoint. -#' -#' ## Authentication -#' API keys together with configurations of LLM providers are -#' stored inside Portkey application. +#' variety of LLMs providers via a single endpoint. #' #' @family chatbots +#' @param model The model name, e.g. `@my-provider/my-model`. #' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. #' @param credentials `r api_key_param("PORTKEY_API_KEY")` -#' @param model `r param_model("gpt-4o", "openai")` -#' @param virtual_key A virtual identifier storing LLM provider's API key. See -#' [documentation](https://portkey.ai/docs/product/ai-gateway/virtual-keys). -#' Can be read from the `PORTKEY_VIRTUAL_KEY` environment variable. +#' @param virtual_key `r lifecycle::badge("deprecated")`. +#' Portkey now recommend supplying the model provider +#' (formerly known as the `virtual_key`), in the model name, e.g. +#' `@my-provider/my-model`. See +#' for details. +#' +#' For backward compatibility, the `PORTKEY_VIRTUAL_KEY` env var is still used +#' if the model +#' you can specify the virtual key here. #' @export #' @inheritParams chat_openai #' @inherit chat_openai return #' @examples #' \dontrun{ -#' chat <- chat_portkey(virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY")) +#' chat <- chat_portkey() #' chat$chat("Tell me three jokes about statisticians") #' } chat_portkey <- function( + model, system_prompt = NULL, base_url = "https://api.portkey.ai/v1", api_key = NULL, credentials = NULL, - virtual_key = portkey_virtual_key(), - model = NULL, + virtual_key = deprecated(), params = NULL, api_args = list(), echo = NULL, api_headers = character() ) { - model <- set_default(model, "gpt-4o") + check_string(model) echo <- check_echo(echo) + if (lifecycle::is_present(virtual_key)) { + lifecycle::deprecate_warn( + when = "0.4.0", + what = "chat_portkey(virtual_key=)", + with = "chat_portkey(model=)", + ) + check_string(virtual_key, allow_null = TRUE) + } else { + virtual_key <- NULL + } + + # For backward compatibility + if (!grepl("^@", model)) { + virtual_key <- virtual_key %||% key_get("PORTKEY_VIRTUAL_KEY") + model <- paste0("@", virtual_key, "/", model) + } + credentials <- as_credentials( "chat_portkey", function() portkey_key(), @@ -54,7 +73,6 @@ chat_portkey <- function( params = params, extra_args = api_args, credentials = credentials, - virtual_key = virtual_key, extra_headers = api_headers ) Chat$new(provider = provider, system_prompt = system_prompt, echo = echo) @@ -62,7 +80,7 @@ chat_portkey <- function( chat_portkey_test <- function( ..., - model = "gpt-4o-mini", + model = "@open-ai-virtual-7f0dcd/gpt-4o-mini", params = NULL, echo = "none" ) { @@ -74,29 +92,13 @@ chat_portkey_test <- function( ProviderPortkeyAI <- new_class( "ProviderPortkeyAI", - parent = ProviderOpenAI, - properties = list( - virtual_key = prop_string(allow_null = TRUE) - ) + parent = ProviderOpenAI ) -portkey_key_exists <- function() { - key_exists("PORTKEY_API_KEY") -} - portkey_key <- function() { key_get("PORTKEY_API_KEY") } -portkey_virtual_key <- function() { - val <- Sys.getenv("PORTKEY_VIRTUAL_KEY") - if (!identical(val, "")) { - val - } else { - NULL - } -} - method(base_request, ProviderPortkeyAI) <- function(provider) { req <- request(provider@base_url) @@ -105,7 +107,6 @@ method(base_request, ProviderPortkeyAI) <- function(provider) { provider@credentials(), "x-portkey-api-key" ) - req <- httr2::req_headers(req, `x-portkey-virtual-key` = provider@virtual_key) req <- ellmer_req_robustify(req) req <- ellmer_req_user_agent(req) req <- base_request_error(provider, req) @@ -117,15 +118,13 @@ method(base_request, ProviderPortkeyAI) <- function(provider) { #' @rdname chat_portkey models_portkey <- function( base_url = "https://api.portkey.ai/v1", - api_key = portkey_key(), - virtual_key = NULL + api_key = portkey_key() ) { provider <- ProviderPortkeyAI( name = "PortkeyAI", model = "", base_url = base_url, - credentials = function() api_key, - virtual_key = virtual_key + credentials = function() api_key ) req <- base_request(provider) @@ -135,11 +134,11 @@ models_portkey <- function( json <- resp_body_json(resp) id <- map_chr(json$data, "[[", "id") - created_at <- as.POSIXct(map_dbl(json$data, "[[", "created_at")) + slug <- map_chr(json$data, "[[", "slug") df <- data.frame( id = id, - created_at = created_at + slug = slug ) - df[order(-xtfrm(df$created_at)), ] + df } diff --git a/man/chat_portkey.Rd b/man/chat_portkey.Rd index e8d5e0fd..6bd1ad64 100644 --- a/man/chat_portkey.Rd +++ b/man/chat_portkey.Rd @@ -6,25 +6,23 @@ \title{Chat with a model hosted on PortkeyAI} \usage{ chat_portkey( + model, system_prompt = NULL, base_url = "https://api.portkey.ai/v1", api_key = NULL, credentials = NULL, - virtual_key = portkey_virtual_key(), - model = NULL, + virtual_key = deprecated(), params = NULL, api_args = list(), echo = NULL, api_headers = character() ) -models_portkey( - base_url = "https://api.portkey.ai/v1", - api_key = portkey_key(), - virtual_key = NULL -) +models_portkey(base_url = "https://api.portkey.ai/v1", api_key = portkey_key()) } \arguments{ +\item{model}{The model name, e.g. \verb{@my-provider/my-model}.} + \item{system_prompt}{A system prompt to set the behavior of the assistant.} \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} @@ -37,13 +35,15 @@ You generally should not need this argument; instead set the \code{PORTKEY_API_K The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} -\item{virtual_key}{A virtual identifier storing LLM provider's API key. See -\href{https://portkey.ai/docs/product/ai-gateway/virtual-keys}{documentation}. -Can be read from the \code{PORTKEY_VIRTUAL_KEY} environment variable.} +\item{virtual_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}. +Portkey now recommend supplying the model provider +(formerly known as the \code{virtual_key}), in the model name, e.g. +\verb{@my-provider/my-model}. See +\url{https://portkey.ai/docs/support/upgrade-to-model-catalog} for details. -\item{model}{The model to use for the chat (defaults to "gpt-4o"). -We regularly update the default, so we strongly recommend explicitly specifying a model for anything other than casual use. -Use \code{models_openai()} to see all options.} +For backward compatibility, the \code{PORTKEY_VIRTUAL_KEY} env var is still used +if the model +you can specify the virtual key here.} \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} @@ -70,16 +70,11 @@ A \link{Chat} object. \description{ \href{https://portkey.ai/docs/product/ai-gateway/universal-api}{PortkeyAI} provides an interface (AI Gateway) to connect through its Universal API to a -variety of LLMs providers with a single endpoint. -\subsection{Authentication}{ - -API keys together with configurations of LLM providers are -stored inside Portkey application. -} +variety of LLMs providers via a single endpoint. } \examples{ \dontrun{ -chat <- chat_portkey(virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY")) +chat <- chat_portkey() chat$chat("Tell me three jokes about statisticians") } } diff --git a/plans/chat-openai-compatible.md b/plans/chat-openai-compatible.md new file mode 100644 index 00000000..bd34c5c2 --- /dev/null +++ b/plans/chat-openai-compatible.md @@ -0,0 +1,271 @@ +# Plan: Rename chat_openai to chat_openai_compatible + +## Overview +Rename `chat_openai()` → `chat_openai_compatible()` and `chat_openai_responses()` → `chat_openai()` to better reflect their purposes. The compatible version is for OpenAI-compatible APIs (not official OpenAI), while the main version uses OpenAI's responses API. + +## Design Decisions +1. **Hard rename** - No deprecation period, breaking change +2. **Remove default base_url** - `chat_openai_compatible()` will require explicit `base_url` parameter +3. **Keep credentials-only API** - New `chat_openai()` won't add `api_key` parameter +4. **Rename test helpers** - Update `_test()` functions to match new names + +## Implementation Steps + +### 1. Core Function Refactoring + +#### R/provider-openai.R → R/provider-openai-compatible.R +- **Rename file**: `provider-openai.R` → `provider-openai-compatible.R` +- Rename `chat_openai()` → `chat_openai_compatible()` +- Rename `chat_openai_test()` → `chat_openai_compatible_test()` +- Rename class: `ProviderOpenAI` → `ProviderOpenAICompatible` +- Remove default value from `base_url` parameter (make it required) +- Update documentation: + - Title: "Chat with OpenAI-compatible API" + - Description: Emphasize this is for OpenAI-compatible APIs (Ollama, vLLM, etc.), NOT for official OpenAI + - Add note directing users to `chat_openai()` for official OpenAI API + - Update `base_url` parameter docs to indicate it's required +- Update `@family chatbots` tag references + +#### R/provider-openai-responses.R → R/provider-openai.R +- **Rename file**: `provider-openai-responses.R` → `provider-openai.R` +- Rename `chat_openai_responses()` → `chat_openai()` +- Rename `chat_openai_responses_test()` → `chat_openai_test()` +- Rename class: `ProviderOpenAIResponses` → `ProviderOpenAI` +- Update documentation: + - Title: "Chat with OpenAI" + - Description: Main function for official OpenAI API using responses endpoint + - Keep all parameters as-is (no `api_key` parameter) + +#### R/provider-openai-models.R (if exists) +- Update `models_openai()` documentation if it references the renamed functions + +### 2. Provider Files Using @inheritParams + +**19 provider files** use `@inheritParams chat_openai` - update to `@inheritParams chat_openai_compatible`: +- R/provider-vllm.R - Update wrapper description +- R/provider-snowflake.R +- R/provider-portkey.R +- R/provider-perplexity.R - Update wrapper description +- R/provider-openrouter.R +- R/provider-ollama.R - Update wrapper description +- R/provider-mistral.R +- R/provider-huggingface.R - Update wrapper description +- R/provider-groq.R - Update wrapper description +- R/provider-github.R +- R/provider-deepseek.R +- R/provider-databricks.R +- R/provider-cloudflare.R +- R/provider-azure.R +- R/provider-aws.R +- R/provider-any.R + +**Note:** Some providers (vLLM, Perplexity, Ollama, Huggingface, Groq) describe themselves as "lightweight wrapper around chat_openai()" - update to say "Uses OpenAI compatible API via `chat_openai_compatible()`" + +### 3. Provider Files Using @inherit return + +**17 provider files** use `@inherit chat_openai return` - keep as is since return type is the same. + +### 4. Test Files + +Update all test files using the renamed functions: + +#### tests/testthat/test-provider-openai.R → tests/testthat/test-provider-openai-compatible.R +- **Rename file**: `test-provider-openai.R` → `test-provider-openai-compatible.R` +- Rename all `chat_openai_test()` → `chat_openai_compatible_test()` (11 instances) +- Update test descriptions/context as needed + +#### tests/testthat/test-provider-openai-responses.R → tests/testthat/test-provider-openai.R +- **Rename file**: `test-provider-openai-responses.R` → `test-provider-openai.R` +- Rename all `chat_openai_responses_test()` → `chat_openai_test()` (10 instances) +- Update test descriptions/context as needed + +#### Other test files using chat_openai_test() +- tests/testthat/test-parallel-chat.R +- tests/testthat/test-batch-chat.R +- tests/testthat/test-content.R +- tests/testthat/test-chat.R +- tests/testthat/test-chat-tools.R +- tests/testthat/test-chat-structured.R +- tests/testthat/test-utils-auth.R +- tests/testthat/test-chat-utils.R + +**Decision:** These tests should continue to use `chat_openai_test()` so they use the new responses API. + +#### Snapshot files +These will auto-update when tests run: +- tests/testthat/_snaps/provider-openai.md → tests/testthat/_snaps/provider-openai-compatible.md +- tests/testthat/_snaps/provider-openai-responses.md → tests/testthat/_snaps/provider-openai.md +- tests/testthat/_snaps/utils-auth.md +- tests/testthat/_snaps/chat.md +- tests/testthat/_snaps/content-replay.md +- tests/testthat/_snaps/batch-chat.md + +### 5. Vignettes + +Should not need changes; continue to use `chat_openai()` in order to use latest API. + +### 6. README Files + +#### README.Rmd (4 usages) +- Update to `chat_openai()` (responses API) +- Use `devtools::build_readme()` to update rendered .md + +### 7. Source Code Documentation + +Update function documentation and examples: + +#### R/live.R +- Update `@param chat` documentation mentioning `chat_openai()` + +#### R/content-image.R +- Update examples using `chat_openai()` + +#### R/batch-chat.R +- Update documentation mentioning `chat_openai()` + +#### R/parallel-chat.R +- Update documentation mentioning `chat_openai()` + +#### R/tools-def-auto.R +- Update `create_tool_def()` defaults using `chat_openai()` + +### 8. Man Pages + +These will be regenerated by `devtools::document()` + +### 9. NAMESPACE + +Update exports: +- Remove: `export(chat_openai)` (OLD meaning) +- Remove: `export(chat_openai_responses)` +- Add: `export(chat_openai)` (NEW meaning - responses API) +- Add: `export(chat_openai_compatible)` + +Note: `devtools::document()` should handle this automatically. + +### 10. NEWS.md + +Add entry at top for next version: +```markdown +* `chat_openai()` has been renamed to `chat_openai_compatible()` to clarify it's for OpenAI-compatible APIs, not the official OpenAI API. The `base_url` parameter is now required (#801). +* `chat_openai_responses()` has been renamed to `chat_openai()`. This is now the main function for using OpenAI's official API via the responses endpoint. +``` + +### 11. _pkgdown.yml + +No work needed. + +### 12. Class Inheritance Considerations + +Rename classes to match new APIs: +- `ProviderOpenAI` → `ProviderOpenAICompatible` +- `ProviderOpenAIResponses` → `ProviderOpenAI` + +This affects all providers that inherit from `ProviderOpenAI`: +- Update their `parent = ProviderOpenAI` to `parent = ProviderOpenAICompatible` +- Providers affected: Groq, Ollama, Perplexity, Huggingface, and any others that currently inherit from ProviderOpenAI + +## Testing Strategy + +1. Run `devtools::document()` to regenerate man pages and NAMESPACE +2. Run `devtools::test(reporter = "check")` to run all tests +3. Update snapshot files as needed +4. Run `pkgdown::check_pkgdown()` to verify all topics in reference +5. Verify examples in vignettes still work +6. Check that cross-references work correctly + +The vcr cassettes will fail; but Hadley will take care of that. + +## Files to Change Summary + +### Core Implementation (4 files) +- R/provider-openai.R → R/provider-openai-compatible.R +- R/provider-openai-responses.R → R/provider-openai.R +- tests/testthat/test-provider-openai.R → tests/testthat/test-provider-openai-compatible.R +- tests/testthat/test-provider-openai-responses.R → tests/testthat/test-provider-openai.R + +### Provider Documentation (19 files) +- All files using `@inheritParams chat_openai` or referencing the function + +### Test Files (9+ files) +- test-provider-openai.R → test-provider-openai-compatible.R +- test-provider-openai-responses.R → test-provider-openai.R +- test-parallel-chat.R +- test-batch-chat.R +- test-content.R +- test-chat.R +- test-chat-tools.R +- test-chat-structured.R +- test-utils-auth.R +- test-chat-utils.R + +### Snapshot Files (2 files) +- _snaps/provider-openai.md → _snaps/provider-openai-compatible.md +- _snaps/provider-openai-responses.md → _snaps/provider-openai.md + +### Documentation Files (6 vignettes + 1 README) +- vignettes/ellmer.Rmd +- vignettes/tool-calling.Rmd +- vignettes/streaming-async.Rmd +- vignettes/programming.Rmd +- vignettes/prompt-design.Rmd +- vignettes/structured-data.Rmd +- README.Rmd + +### Source Code Docs (5 files) +- R/live.R +- R/content-image.R +- R/batch-chat.R +- R/parallel-chat.R +- R/tools-def-auto.R + +### Other (1 file) +- NEWS.md + +## Estimated Total Files: ~45 files + +## Risks and Mitigations + +### High Risk +1. **Breaking change for users** - All code using `chat_openai()` will break + - Mitigation: Clear NEWS.md entry, consider blog post/announcement + +2. **Documentation inheritance cascade** - 19 files use `@inheritParams` + - Mitigation: Careful find-replace, thorough testing of `devtools::document()` + +### Medium Risk +1. **Test disruption** - Many tests use `chat_openai_test()` + - Mitigation: Update systematically, run tests frequently + +2. **Snapshot mismatches** - Function names in snapshots will change + - Mitigation: Review and approve snapshot updates carefully + +### Low Risk +1. **Cross-reference links** - `@family chatbots` should auto-update + - Mitigation: Verify after `devtools::document()` + +## Open Questions + +1. Should general tests use `chat_openai_test()` (responses API) or `chat_openai_compatible_test()`? + - **Recommendation:** Use `chat_openai_test()` (responses API) as the primary test function since it's the main OpenAI interface + +2. Should we update `OPENAI_BASE_URL` environment variable logic in the compatible version? + - Current behavior: Falls back to `OPENAI_BASE_URL` if set + - Keep this in `chat_openai_compatible()` but drop from `chat_openai()` + +3. How to handle the `service_tier` parameter that's unique to responses API? + - It's only in `chat_openai()` (responses), not in `chat_openai_compatible()` + - No work needed + +## Success Criteria + +- [ ] All functions renamed correctly +- [ ] `base_url` parameter is required in `chat_openai_compatible()` +- [ ] Documentation clearly distinguishes compatible API vs official OpenAI API +- [ ] All tests pass +- [ ] All vignettes knit successfully +- [ ] `pkgdown::check_pkgdown()` passes +- [ ] README.md updated +- [ ] NEWS.md entry added +- [ ] No broken cross-references in documentation +- [ ] Snapshot tests updated and reviewed diff --git a/tests/testthat/_snaps/provider-portkey.md b/tests/testthat/_snaps/provider-portkey.md deleted file mode 100644 index 0af59bb3..00000000 --- a/tests/testthat/_snaps/provider-portkey.md +++ /dev/null @@ -1,7 +0,0 @@ -# defaults are reported - - Code - . <- chat_portkey() - Message - Using model = "gpt-4o". - diff --git a/tests/testthat/test-provider-portkey.R b/tests/testthat/test-provider-portkey.R index 870d4763..498cf34c 100644 --- a/tests/testthat/test-provider-portkey.R +++ b/tests/testthat/test-provider-portkey.R @@ -1,53 +1,33 @@ # Getting started -------------------------------------------------------- test_that("can make simple request", { - chat <- chat_portkey_test( - virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY"), - "Be as terse as possible; no punctuation" - ) + chat <- chat_portkey_test("Be as terse as possible; no punctuation") resp <- chat$chat("What is 1 + 1?", echo = FALSE) expect_match(resp, "2") expect_equal(unname(chat$last_turn()@tokens[1:2] > 0), c(TRUE, TRUE)) }) test_that("can make simple streaming request", { - chat <- chat_portkey_test( - virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY"), - "Be as terse as possible; no punctuation" - ) + chat <- chat_portkey_test("Be as terse as possible; no punctuation") resp <- coro::collect(chat$stream("What is 1 + 1?")) expect_match(paste0(unlist(resp), collapse = ""), "2") }) # Common provider interface ----------------------------------------------- -test_that("defaults are reported", { - expect_snapshot(. <- chat_portkey()) -}) - test_that("supports tool calling", { - chat_fun <- \(...) { - chat_portkey_test(virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY")) - } - + chat_fun <- chat_portkey_test test_tools_simple(chat_fun) }) test_that("can extract data", { - chat_fun <- \(...) { - chat_portkey_test(virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY")) - } - + chat_fun <- chat_portkey_test test_data_extraction(chat_fun) }) test_that("can use images", { chat_fun <- \(...) { - chat_portkey_test( - virtual_key = Sys.getenv("PORTKEY_VIRTUAL_KEY"), - model = "gpt-4.1-mini", - ... - ) + chat_portkey_test(model = "@open-ai-virtual-7f0dcd/gpt-4.1-mini", ...) } test_images_inline(chat_fun)