diff --git a/.gitignore b/.gitignore index 03fd37c..8d152cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ current_context.txt current_context.config contexts/test.txt -src/__pycache__/ -src/openaiapirc \ No newline at end of file +__pycache__/ +src/openaiapirc diff --git a/Installation.md b/Installation.md index 7b66752..0cfd685 100644 --- a/Installation.md +++ b/Installation.md @@ -19,24 +19,22 @@ In order to run the Codex CLI, ensure that you have python installed. To install python -m pip install -r requirements.txt ``` -Additionally, the OpenAI API key, organization id, and engine id are required to execute the Codex CLI tool. +Additionally, a MiniMax API key, API base URL, and model id are required to execute the Codex CLI tool. An organization id is optional. -To obtain the OpenAI API key information, go to (https://beta.openai.com/account/api-keys) and login into your account. +## MiniMax configuration -Once logged in you will see: -![](images/OpenAI-apikey.png) +The setup scripts accept an API base URL and model id. Use one of these regional base URLs: -Copy the API key by clicking the _Copy_ button and save the copied key where you can retrieve it. +* Global: `https://api.minimax.io/v1` +* China: `https://api.minimaxi.com/v1` -To obtain the OpenAI organization id, go to OpenAI Settings page (https://beta.openai.com/account/org-settings) and copy the id shown underneath the _Organization ID_ heading. Save the copied id with the API key that was stored in previous step. +The current model ids are `MiniMax-M3` and `MiniMax-M2.7`. For example: -See image below for reference: -![](images/OpenAI-orgid.png) - -To obtain the OpenAI engine id, go to OpenAI Engines page (https://beta.openai.com/docs/engines/codex-series-private-beta) for the engines available for Codex models. Select the desired engine and save the engine id with the API key and organization id stored in previous steps. +```bash +source bash_setup.sh -k YOUR_API_KEY -b https://api.minimax.io/v1 -m MiniMax-M3 +``` -See image below for reference: -![](images/OpenAI-engineid.png) +The generated `src/openaiapirc` stores `base_url` and `model`; `organization_id` is optional. ## Bash instructions @@ -57,7 +55,7 @@ To leverage the Codex CLI on WSL and Linux environments using Bash please follow In the Codex CLI folder, there is a folder named `scripts` where you will find a `bash_setup.sh` script which is used to setup the Bash environment. Run the following commands to setup your environment, the script will prompt - for Organization id, API key and engine id: + for an optional organization id, API key, API base URL, and model id: ``` cd scripts source bash_setup.sh @@ -82,13 +80,15 @@ By default `bash_setup.sh` will prompt for the required settings. It's also poss | Parameter | Description | |--------------|-------------| | `-o ` | To pass your [OpenAI Organization Id](https://beta.openai.com/account/org-settings) | -| `-k ` | To pass your [OpenAI API key](https://beta.openai.com/account/api-keys) | -| `-e ` | To specify the [OpenAI Engine Id](https://beta.openai.com/docs/engines/codex-series-private-beta) | +| `-k ` | To pass your API key | +| `-b ` | To specify the API base URL | +| `-m ` | To specify the model id | +| `-e ` | Legacy alias for `-m` | Example: ``` -source bash_setup.sh -o myorgid -k myapikey -e someengineid +source bash_setup.sh -k YOUR_API_KEY -b https://api.minimax.io/v1 -m MiniMax-M3 ``` For help running the Codex CLI Bash setup, please run the following command: @@ -106,7 +106,7 @@ source bash_setup.sh -h $ git clone https://github.com/microsoft/Codex-CLI.git ~/your/custom/path/ ``` -2. In zsh, go to `~/your/custom/path/` (the folder contains Codex CLI code), then run the following command to setup your zsh environment. The script will prompt for Organization id, API key and engine id: +2. In zsh, go to `~/your/custom/path/` (the folder contains Codex CLI code), then run the following command to setup your zsh environment. The script will prompt for an optional organization id, API key, API base URL, and model id: ``` ./scripts/zsh_setup.sh @@ -126,8 +126,10 @@ Once you are done, go to `~/your/custom/path/` (the folder contains Codex CLI co | Parameter | Description | |--------------|-------------| | `-o ` | To pass your [OpenAI Organization Id](https://beta.openai.com/account/org-settings) | -| `-k ` | To pass your [OpenAI API key](https://beta.openai.com/account/api-keys) | -| `-e ` | To specify the [OpenAI Engine Id](https://beta.openai.com/docs/engines/codex-series-private-beta) | +| `-k ` | To pass your API key | +| `-b ` | To specify the API base URL | +| `-m ` | To specify the model id | +| `-e ` | Legacy alias for `-m` | ## Powershell instructions @@ -148,10 +150,10 @@ For more information about Execution Policies, see [about_Execution_Policies](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies). -3. In the same Powershell terminal, go to `C:\your\custom\path\Codex-CLI\` (the folder that contains the cloned Codex CLI project). Copy the following command, then replace `YOUR_OPENAI_ORGANIZATION_ID` and `ENGINE_ID` with your OpenAI organization Id and OpenAI engine Id. Run the command to setup your PowerShell environment. It will prompt you for OpenAI access key. +3. In the same Powershell terminal, go to `C:\your\custom\path\Codex-CLI\` (the folder that contains the cloned Codex CLI project). Run the command below with your API base URL and model id. It will prompt you for an API key. ```PowerShell -.\scripts\powershell_setup.ps1 -OpenAIOrganizationId 'YOUR_OPENAI_ORGANIZATION_ID' -OpenAIEngineId 'ENGINE_ID' +.\scripts\powershell_setup.ps1 -OpenAIApiBaseUrl 'https://api.minimax.io/v1' -OpenAIModelId 'MiniMax-M3' ```           See [About powershell_setup.ps1](#about-powershell_setupps1) section to learn script parameters. @@ -173,6 +175,7 @@ Set-ExecutionPolicy Undefined -Scope CurrentUser | Parameter | Type | Description | |--|--|--| | `-OpenAIApiKey` | [SecureString](https://docs.microsoft.com/en-us/dotnet/api/system.security.securestring) | Required. If is not supplied, the script will prompt you to input the value. You can find this value at [https://beta.openai.com/account/api-keys](https://beta.openai.com/account/api-keys). To provide the value via PowerShell parameter, this is an example for PowerShell 7:
`.\scripts\powershell_setup.ps1 -OpenAIApiKey (ConvertTo-SecureString "YOUR_OPENAI_API_KEY" -AsPlainText -Force)` | -| `-OpenAIOrganizationId` | String | Required. Your [OpenAI organization Id](https://beta.openai.com/account/org-settings). | -| `-OpenAIEngineId` | String | Required. The [OpenAI engine Id](https://beta.openai.com/docs/engines/codex-series-private-beta) that provides access to a model.| +| `-OpenAIOrganizationId` | String | Optional organization id. | +| `-OpenAIApiBaseUrl` | String | Required API base URL. | +| `-OpenAIModelId` | String | Required model id. `-OpenAIEngineId` remains available as an alias. | | `-RepoRoot` | [FileInfo](https://docs.microsoft.com/en-us/dotnet/api/system.io.fileinfo) | Optional. Default to the current folder.
The value should be the path of Codex CLI folder. Example:
`.\scripts\powershell_setup.ps1 -RepoRoot 'C:\your\custom\path'`| diff --git a/README.md b/README.md index 4f5cddd..3827841 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ This repository aims to grow the understanding of using Codex in applications by ## Requirements * [Python 3.7.1+](https://www.python.org/downloads/) * \[Windows\]: Python is added to PATH. -* An [OpenAI account](https://openai.com/api/) - * [OpenAI API Key](https://beta.openai.com/account/api-keys). - * [OpenAI Organization Id](https://beta.openai.com/account/org-settings). If you have multiple organizations, please update your [default organization](https://beta.openai.com/account/api-keys) to the one that has access to codex engines before getting the organization Id. - * [OpenAI Engine Id](https://beta.openai.com/docs/engines/codex-series-private-beta). It provides access to a model. For example, `code-davinci-002` or `code-cushman-001`. See [here](#what-openai-engines-are-available-to-me) for checking available engines. +* MiniMax API credentials + * API key. + * API base URL: `https://api.minimax.io/v1` globally or `https://api.minimaxi.com/v1` in China. + * Model id: `MiniMax-M3` or `MiniMax-M2.7`. ## Installation @@ -68,7 +68,7 @@ Any time the model seems to output consistently incorrect commands, you can use | `set ` | Sets the configuration of your interaction with the model | -Feel free to improve your experience by changing the token limit, engine id and temperature using the set command. For example, `# set engine cushman-codex`, `# set temperature 0.5`, `# set max_tokens 50`. +Feel free to improve your experience by changing the token limit, model id and temperature using the set command. For example, `# set model MiniMax-M3`, `# set temperature 0.5`, `# set max_tokens 50`. ## Prompt Engineering and Context Files @@ -114,27 +114,9 @@ Use `DEBUG_MODE` to use a terminal input instead of the stdin and debug the code Sometimes the `openai` package will throws errors that aren't caught by the tool, you can add a catch block at the end of `codex_query.py` for that exception and print a custom error message. ## FAQ -### What OpenAI engines are available to me? -You might have access to different [OpenAI engines](https://beta.openai.com/docs/api-reference/engines) per OpenAI organization. To check what engines are available to you, one can query the [List engines API](https://beta.openai.com/docs/api-reference/engines/list) for available engines. See the following commands: - -* Shell - ``` - curl https://api.openai.com/v1/engines \ - -H 'Authorization: Bearer YOUR_API_KEY' \ - -H 'OpenAI-Organization: YOUR_ORG_ID' - ``` - -* PowerShell - - PowerShell v5 (The default one comes with Windows) - ```powershell - (Invoke-WebRequest -Uri https://api.openai.com/v1/engines -Headers @{"Authorization" = "Bearer YOUR_API_KEY"; "OpenAI-Organization" = "YOUR_ORG_ID"}).Content - ``` - - PowerShell v7 - ```powershell - (Invoke-WebRequest -Uri https://api.openai.com/v1/engines -Authentication Bearer -Token (ConvertTo-SecureString "YOUR_API_KEY" -AsPlainText -Force) -Headers @{"OpenAI-Organization" = "YOUR_ORG_ID"}).Content - ``` +### Which MiniMax models are supported? + +Set `model` to `MiniMax-M3` or `MiniMax-M2.7` in `src/openaiapirc`. The client sends requests to the configured `base_url` using the chat completions API. ### Can I run the sample on Azure? The sample code can be currently be used with Codex on OpenAI’s API. In the coming months, the sample will be updated so you can also use it with the [Azure OpenAI Service](https://aka.ms/azure-openai). diff --git a/requirements.txt b/requirements.txt index 2f5d837..ab19157 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -openai>=0.18.1 +openai>=1.0.0 psutil>=5.9.0 diff --git a/scripts/bash_setup.sh b/scripts/bash_setup.sh index acf228c..2e4dfbb 100755 --- a/scripts/bash_setup.sh +++ b/scripts/bash_setup.sh @@ -12,7 +12,9 @@ Usage: source bash_setup.sh [optional parameters] -o orgId Set the OpenAI organization id. -k apiKey Set the OpenAI API key. - -e engineId Set the OpenAI engine id. + -b baseUrl Set the API base URL. + -m modelId Set the model id. + -e modelId Legacy alias for -m. -d Print some system information for debugging. -h Print this help content. @@ -28,7 +30,8 @@ readParameters() case $1 in -o ) shift; ORG_ID=$1 ;; -k ) shift; SECRET_KEY=$1 ;; - -e ) shift; ENGINE_ID=$1 ;; + -b ) shift; BASE_URL=$1 ;; + -m | -e ) shift; MODEL_ID=$1 ;; -d ) systemInfo exitScript ;; @@ -50,35 +53,35 @@ askSettings() if [ -z "$SECRET_KEY" ]; then echo -n 'OpenAI API key: '; read -s SECRET_KEY; echo fi - if [ -z "$ENGINE_ID" ]; then - echo -n 'OpenAI Engine Id: '; read ENGINE_ID + if [ -z "$BASE_URL" ]; then + echo -n 'API base URL: '; read BASE_URL + fi + if [ -z "$MODEL_ID" ]; then + echo -n 'Model Id: '; read MODEL_ID fi } -# Call OpenAI API with the given settings to verify everythin is in order +# Validate the settings before writing the configuration file validateSettings() { - echo -n "*** Testing Open AI access... " - local TEST=$(curl -s 'https://api.openai.com/v1/engines' -H "Authorization: Bearer $SECRET_KEY" -H "OpenAI-Organization: $ORG_ID" -w '%{http_code}') - local STATUS_CODE=$(echo "$TEST"|tail -n 1) - if [ $STATUS_CODE -ne 200 ]; then - echo "ERROR [$STATUS_CODE]" - echo "Failed to access OpenAI API, result: $STATUS_CODE" - echo "Please check your OpenAI API key (https://beta.openai.com/account/api-keys)" - echo "and Organization ID (https://beta.openai.com/account/org-settings)." - echo "*************" - exitScript - return - fi - local ENGINE_FOUND=$(echo "$TEST"|grep '"id"'|grep "\"$ENGINE_ID\"") - if [ -z "$ENGINE_FOUND" ]; then + echo -n "*** Validating API settings... " + if [ -z "$SECRET_KEY" ] || [ -z "$BASE_URL" ] || [ -z "$MODEL_ID" ]; then echo "ERROR" - echo "Cannot find OpenAI engine: $ENGINE_ID" - echo "Please check the OpenAI engine id (https://beta.openai.com/docs/engines/codex-series-private-beta)." + echo "API key, base URL, and model id are required." echo "*************" exitScript return fi + case "$BASE_URL" in + http://* | https://* ) ;; + * ) + echo "ERROR" + echo "API base URL must start with http:// or https://." + echo "*************" + exitScript + return + ;; + esac echo "OK ***" } @@ -89,7 +92,8 @@ configureApp() echo '[openai]' > $OPENAI_RC_FILE echo "organization_id=$ORG_ID" >> $OPENAI_RC_FILE echo "secret_key=$SECRET_KEY" >> $OPENAI_RC_FILE - echo "engine=$ENGINE_ID" >> $OPENAI_RC_FILE + echo "base_url=$BASE_URL" >> $OPENAI_RC_FILE + echo "model=$MODEL_ID" >> $OPENAI_RC_FILE chmod +x "$CODEX_CLI_PATH/src/codex_query.py" } @@ -149,7 +153,7 @@ systemInfo() # Remove variables and functions from the environment, in case the script was sourced cleanupEnv() { - unset ORG_ID SECRET_KEY ENGINE_ID SOURCED OPENAI_RC_FILE BASH_RC_FILE + unset ORG_ID SECRET_KEY BASE_URL MODEL_ID SOURCED OPENAI_RC_FILE BASH_RC_FILE unset -f askSettings validateSettings configureApp configureBash enableApp readParameters } diff --git a/scripts/powershell_setup.ps1 b/scripts/powershell_setup.ps1 index 1291457..49a2697 100644 --- a/scripts/powershell_setup.ps1 +++ b/scripts/powershell_setup.ps1 @@ -18,13 +18,17 @@ param [SecureString] $OpenAIApiKey, + [Parameter()] + $OpenAIOrganizationId, + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - $OpenAIOrganizationId, + $OpenAIApiBaseUrl, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - $OpenAIEngineId + [Alias("OpenAIEngineId")] + $OpenAIModelId ) $plugInScriptPath = Join-Path $RepoRoot -ChildPath "scripts\powershell_plugin.ps1" @@ -42,25 +46,11 @@ if ($PSMajorVersion -lt 7) { $openAIApiKeyPlainText = ConvertFrom-SecureString -SecureString $OpenAIApiKey -AsPlainText } -# Check the access with OpenAI API -Write-Host "Checking OpenAI access..." -$enginesApiUri = "https://api.openai.com/v1/engines" -$response = $null -try { - if ($PSMajorVersion -lt 7) { - $response = (Invoke-WebRequest -Uri $enginesApiUri -Headers @{"Authorization" = "Bearer $openAIApiKeyPlainText"; "OpenAI-Organization" = "$OpenAIOrganizationId"}) - } else { - $response = (Invoke-WebRequest -Uri $enginesApiUri -Authentication Bearer -Token $OpenAIApiKey -Headers @{"OpenAI-Organization" = "$OpenAIOrganizationId"}) - } -} catch { - $statusCode = $_.Exception.Response.StatusCode.value__ - Write-Error "Failed to access OpenAI api [$statusCode]. Please check your OpenAI API key (https://beta.openai.com/account/api-keys) and Organization ID (https://beta.openai.com/account/org-settings)." - exit 1 -} - -# Check if target engine is available to the user -if ($null -eq (($response.Content | ConvertFrom-Json).data | Where-Object {$_.id -eq $OpenAIEngineId})) { - Write-Error "Cannot find OpenAI engine: $OpenAIEngineId. Please check the OpenAI engine id (https://beta.openai.com/docs/engines/codex-series-private-beta) and your Organization ID (https://beta.openai.com/account/org-settings)." +# Validate the API base URL before writing the configuration file +$apiBaseUri = $null +if (-not [Uri]::TryCreate($OpenAIApiBaseUrl, [UriKind]::Absolute, [ref]$apiBaseUri) -or + $apiBaseUri.Scheme -notin @("http", "https")) { + Write-Error "API base URL must be an absolute HTTP or HTTPS URL." exit 1 } @@ -90,7 +80,8 @@ if (!(Test-Path -Path $openAIConfigPath)) { Set-Content -Path $openAIConfigPath "[openai] organization_id=$OpenAIOrganizationId secret_key=$openAIApiKeyPlainText -engine=$OpenAIEngineId" +base_url=$OpenAIApiBaseUrl +model=$OpenAIModelId" Write-Host "Updated OpenAI configuration file with secrets." Write-Host -ForegroundColor Blue "Codex CLI PowerShell (v$PSMajorVersion) setup completed. Please open a new PowerShell session, type in # followed by your natural language command and hit Ctrl+G!" diff --git a/scripts/zsh_setup.sh b/scripts/zsh_setup.sh index 312fc0f..6b63cef 100755 --- a/scripts/zsh_setup.sh +++ b/scripts/zsh_setup.sh @@ -5,37 +5,34 @@ # You can pass the following arguments to the script: # -o: Your OpenAI organization id. # -k: Your OpenAI API key. -# -e: The OpenAI engine id that provides access to a model. +# -b: The API base URL. +# -m: The model id. +# -e: Legacy alias for -m. # # For example: -# ./zsh_setup.sh -o -k -e +# ./zsh_setup.sh -o -k -b -m # set -e -# Call OpenAI API with the given settings to verify everythin is in order +# Validate the settings before writing the configuration file validateSettings() { - echo -n "*** Testing Open AI access... " - local TEST=$(curl -s 'https://api.openai.com/v1/engines' -H "Authorization: Bearer $secret" -H "OpenAI-Organization: $orgId" -w '%{http_code}') - local STATUS_CODE=$(echo "$TEST"|tail -n 1) - if [ $STATUS_CODE -ne 200 ]; then - echo "ERROR [$STATUS_CODE]" - echo "Failed to access OpenAI API, result: $STATUS_CODE" - echo "Please check your OpenAI API key (https://beta.openai.com/account/api-keys)" - echo "and Organization ID (https://beta.openai.com/account/org-settings)." - echo "*************" - - exit 1 - fi - local ENGINE_FOUND=$(echo "$TEST"|grep '"id"'|grep "\"$engineId\"") - if [ -z "$ENGINE_FOUND" ]; then + echo -n "*** Validating API settings... " + if [ -z "$secret" ] || [ -z "$baseUrl" ] || [ -z "$modelId" ]; then echo "ERROR" - echo "Cannot find OpenAI engine: $engineId" - echo "Please check the OpenAI engine id (https://beta.openai.com/docs/engines/codex-series-private-beta)." + echo "API key, base URL, and model id are required." echo "*************" - exit 1 fi + case "$baseUrl" in + http://* | https://* ) ;; + * ) + echo "ERROR" + echo "API base URL must start with http:// or https://." + echo "*************" + exit 1 + ;; + esac echo "OK ***" } @@ -62,7 +59,8 @@ configureApp() echo "[openai]" > $openAIConfigPath echo "organization_id=$orgId" >> $openAIConfigPath echo "secret_key=$secret" >> $openAIConfigPath - echo "engine=$engineId" >> $openAIConfigPath + echo "base_url=$baseUrl" >> $openAIConfigPath + echo "model=$modelId" >> $openAIConfigPath echo "Updated OpenAI configuration file ($openAIConfigPath) with secrets" @@ -76,6 +74,8 @@ configureApp() zmodload zsh/zutil zparseopts -E -D -- \ o:=o_orgId \ + b:=o_baseUrl \ + m:=o_modelId \ e:=o_engineId \ k:=o_key @@ -85,10 +85,18 @@ else echo -n 'OpenAI Organization Id: '; read orgId fi -if (( ${+o_engineId[2]} )); then - engineId=${o_engineId[2]} +if (( ${+o_baseUrl[2]} )); then + baseUrl=${o_baseUrl[2]} else - echo -n 'OpenAI Engine Id: '; read engineId + echo -n 'API base URL: '; read baseUrl +fi + +if (( ${+o_modelId[2]} )); then + modelId=${o_modelId[2]} +elif (( ${+o_engineId[2]} )); then + modelId=${o_engineId[2]} +else + echo -n 'Model Id: '; read modelId fi if (( ${+o_key[2]} )); then @@ -116,4 +124,4 @@ echo -e "*** Setup complete! ***\n"; echo "***********************************************" echo "Open a new zsh terminal, type '#' followed by" echo "your natural language command and hit Ctrl + G!" -echo "***********************************************" \ No newline at end of file +echo "***********************************************" diff --git a/src/codex_query.py b/src/codex_query.py index ade2a9a..78a1690 100755 --- a/src/codex_query.py +++ b/src/codex_query.py @@ -1,21 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import openai -import sys -import os import configparser +import os import re +import sys +from pathlib import Path + +import openai import psutil -from pathlib import Path from prompt_file import PromptFile from commands import get_command_result MULTI_TURN = "off" SHELL = "" -ENGINE = '' +MODEL = '' +CLIENT = None TEMPERATURE = 0 MAX_TOKENS = 300 @@ -30,9 +32,10 @@ # Read the secret_key from the ini file ~/.config/openaiapirc # The format is: # [openai] -# organization= +# organization_id= # secret_key= -# engine= +# base_url= +# model= def create_template_ini_file(): """ If the ini file does not exist create it and add secret_key @@ -41,28 +44,48 @@ def create_template_ini_file(): print('# Please create a file at {} and add your secret key'.format(API_KEYS_LOCATION)) print('# The format is:\n') print('# [openai]') - print('# organization_id=') + print('# organization_id=') print('# secret_key=\n') - print('# engine=') + print('# base_url=') + print('# model=') sys.exit(1) + +def get_setting(settings, key, fallback=''): + value = settings.get(key, fallback) + return value.strip().strip('"').strip("'") + + def initialize(): """ Initialize openAI and shell mode """ - global ENGINE + global CLIENT + global MODEL # Check if file at API_KEYS_LOCATION exists create_template_ini_file() config = configparser.ConfigParser() config.read(API_KEYS_LOCATION) - openai.api_key = config['openai']['secret_key'].strip('"').strip("'") - openai.organization = config['openai']['organization_id'].strip('"').strip("'") - ENGINE = config['openai']['engine'].strip('"').strip("'") + settings = config['openai'] + api_key = get_setting(settings, 'secret_key') + base_url = get_setting(settings, 'base_url') + organization_id = get_setting(settings, 'organization_id') + MODEL = get_setting(settings, 'model') or get_setting(settings, 'engine') + + if not api_key or not MODEL: + raise ValueError('secret_key and model are required in openaiapirc') + + client_options = {'api_key': api_key} + if base_url: + client_options['base_url'] = base_url.rstrip('/') + if organization_id: + client_options['organization'] = organization_id + CLIENT = openai.OpenAI(**client_options) prompt_config = { - 'engine': ENGINE, + 'model': MODEL, 'temperature': TEMPERATURE, 'max_tokens': MAX_TOKENS, 'shell': SHELL, @@ -72,63 +95,25 @@ def initialize(): return PromptFile(PROMPT_CONTEXT.name, prompt_config) -def is_sensitive_content(content): - """ - Check if the content contains sensitive content - Refer to https://beta.openai.com/docs/engines/content-filter for explanation - """ - if len(content) == 0: - return False - - response = openai.Completion.create( - engine="content-filter-alpha", - prompt = "<|endoftext|>"+content+"\n--\nLabel:", - temperature=0, - max_tokens=1, - top_p=0, - logprobs=10 - ) - - output_label = response["choices"][0]["text"] - - # This is the probability at which we evaluate that a "2" is likely real - # vs. should be discarded as a false positive - toxic_threshold = -0.355 - - if output_label == "2": - # If the model returns "2", return its confidence in 2 or other output-labels - logprobs = response["choices"][0]["logprobs"]["top_logprobs"][0] - - # If the model is not sufficiently confident in "2", - # choose the most probable of "0" or "1" - # Guaranteed to have a confidence for 2 since this was the selected token. - if logprobs["2"] < toxic_threshold: - logprob_0 = logprobs.get("0", None) - logprob_1 = logprobs.get("1", None) - - # If both "0" and "1" have probabilities, set the output label - # to whichever is most probable - if logprob_0 is not None and logprob_1 is not None: - if logprob_0 >= logprob_1: - output_label = "0" - else: - output_label = "1" - # If only one of them is found, set output label to that one - elif logprob_0 is not None: - output_label = "0" - elif logprob_1 is not None: - output_label = "1" - - # If neither "0" or "1" are available, stick with "2" - # by leaving output_label unchanged. - - # if the most probable token is none of "0", "1", or "2" - # this should be set as unsafe - if output_label not in ["0", "1", "2"]: - output_label = "2" - - return (output_label != "0") +def create_chat_completion(client, config, prompt): + response = client.chat.completions.create( + model=config['model'], + messages=[ + { + 'role': 'system', + 'content': ( + "Return a valid {} command for the user's request. " + "Prefix any textual explanation with the shell's comment syntax." + ).format(config['shell']) + }, + {'role': 'user', 'content': prompt} + ], + temperature=config['temperature'], + max_tokens=config['max_tokens'] + ) + return response.choices[0].message.content or '' + def get_query(prompt_file): """ @@ -152,6 +137,7 @@ def get_query(prompt_file): else: sys.exit(0) + def detect_shell(): global SHELL global PROMPT_CONTEXT @@ -168,6 +154,7 @@ def detect_shell(): if shell_prompt_file.is_file(): PROMPT_CONTEXT = shell_prompt_file + if __name__ == '__main__': detect_shell() prompt_file = initialize() @@ -176,7 +163,7 @@ def detect_shell(): user_query, prompt_file = get_query(prompt_file) config = prompt_file.config if prompt_file else { - 'engine': ENGINE, + 'model': MODEL, 'temperature': TEMPERATURE, 'max_tokens': MAX_TOKENS, 'shell': SHELL, @@ -200,28 +187,21 @@ def detect_shell(): codex_query = prefix + prompt_file.read_prompt_file(user_query) + user_query - # get the response from codex - response = openai.Completion.create(engine=config['engine'], prompt=codex_query, temperature=config['temperature'], max_tokens=config['max_tokens'], stop="#") - - completion_all = response['choices'][0]['text'] - - if is_sensitive_content(user_query + '\n' + completion_all): - print("\n# Sensitive content detected, response has been redacted") - else: - print(completion_all) + completion_all = create_chat_completion(CLIENT, config, codex_query) + print(completion_all) - # append output to prompt context file - if config['multi_turn'] == "on": - if completion_all != "" or len(completion_all) > 0: - prompt_file.add_input_output_pair(user_query, completion_all) + # append output to prompt context file + if config['multi_turn'] == "on": + if completion_all != "" or len(completion_all) > 0: + prompt_file.add_input_output_pair(user_query, completion_all) except FileNotFoundError: print('\n\n# Codex CLI error: Prompt file not found, try again') - except openai.error.RateLimitError: + except openai.RateLimitError: print('\n\n# Codex CLI error: Rate limit exceeded, try later') - except openai.error.APIConnectionError: + except openai.APIConnectionError: print('\n\n# Codex CLI error: API connection error, are you connected to the internet?') - except openai.error.InvalidRequestError as e: + except openai.BadRequestError as e: print('\n\n# Codex CLI error: Invalid request - ' + str(e)) except Exception as e: print('\n\n# Codex CLI error: Unexpected exception - ' + str(e)) diff --git a/src/commands.py b/src/commands.py index 4d0bde3..c08d1fb 100644 --- a/src/commands.py +++ b/src/commands.py @@ -16,7 +16,7 @@ def get_command_result(input, prompt_file): - save context - clear context - load context - - set engine + - set model - set temperature - set max_tokens - set shell @@ -58,12 +58,12 @@ def get_command_result(input, prompt_file): return "config set", prompt_file else: return "", prompt_file - elif input.__contains__("engine"): + elif input.__contains__("model") or input.__contains__("engine"): input = input.split() if len(input) == 4: - config['engine'] = input[3] + config['model'] = input[3] prompt_file.set_config(config) - print("# Engine set to " + str(config['engine'])) + print("# Model set to " + str(config['model'])) return "config set", prompt_file else: return "", prompt_file diff --git a/src/prompt_file.py b/src/prompt_file.py index f86aeac..7be8f08 100644 --- a/src/prompt_file.py +++ b/src/prompt_file.py @@ -42,7 +42,7 @@ def read_config(self): lines = f.readlines() config = { - 'engine': lines[0].split(':')[1].strip(), + 'model': lines[0].split(':')[1].strip(), 'temperature': float(lines[1].split(':')[1].strip()), 'max_tokens': int(lines[2].split(':')[1].strip()), 'shell': lines[3].split(':')[1].strip(), @@ -60,7 +60,7 @@ def set_config(self, config): self.config = config with open(self.config_path, 'w') as f: - f.write('engine: {}\n'.format(self.config['engine'])) + f.write('model: {}\n'.format(self.config['model'])) f.write('temperature: {}\n'.format(self.config['temperature'])) f.write('max_tokens: {}\n'.format(self.config['max_tokens'])) f.write('shell: {}\n'.format(self.config['shell'])) @@ -228,13 +228,15 @@ def load_context(self, filename, initialize=False): with filepath.open('r') as f: lines = f.readlines() - # read in the engine name from openaiapirc + # read in the model name from openaiapirc config = configparser.ConfigParser() config.read(API_KEYS_LOCATION) - ENGINE = config['openai']['engine'].strip('"').strip("'") + MODEL = config['openai'].get( + 'model', config['openai'].get('engine', '') + ).strip('"').strip("'") config = { - 'engine': ENGINE, + 'model': MODEL, 'temperature': float(lines[1].split(':')[1].strip()), 'max_tokens': int(lines[2].split(':')[1].strip()), 'shell': lines[3].split(':')[1].strip(), @@ -259,4 +261,4 @@ def load_context(self, filename, initialize=False): print('\n# Context loaded from {}'.format(filename)) else: print("\n# File not found") - return False \ No newline at end of file + return False diff --git a/tests/test_codex_query.py b/tests/test_codex_query.py new file mode 100644 index 0000000..d11adfb --- /dev/null +++ b/tests/test_codex_query.py @@ -0,0 +1,139 @@ +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / 'src')) + +import codex_query +from commands import get_command_result +from prompt_file import PromptFile + + +class InitializeTests(unittest.TestCase): + def initialize_with(self, base_url, model): + config_text = ( + '[openai]\n' + 'organization_id=\n' + 'secret_key=test-key\n' + 'base_url={}\n' + 'model={}\n' + ).format(base_url, model) + + with tempfile.TemporaryDirectory() as directory: + config_path = Path(directory) / 'openaiapirc' + config_path.write_text(config_text, encoding='utf-8') + with patch.object(codex_query, 'API_KEYS_LOCATION', str(config_path)): + with patch.object(codex_query, 'PromptFile') as prompt_file: + with patch.object(codex_query.openai, 'OpenAI') as client: + codex_query.initialize() + + return client, prompt_file + + def test_initialize_configures_global_endpoint(self): + client, prompt_file = self.initialize_with( + 'https://api.minimax.io/v1/', + 'MiniMax-M3' + ) + + client.assert_called_once_with( + api_key='test-key', + base_url='https://api.minimax.io/v1' + ) + self.assertEqual(prompt_file.call_args.args[1]['model'], 'MiniMax-M3') + + def test_initialize_configures_china_endpoint(self): + client, prompt_file = self.initialize_with( + 'https://api.minimaxi.com/v1', + 'MiniMax-M2.7' + ) + + client.assert_called_once_with( + api_key='test-key', + base_url='https://api.minimaxi.com/v1' + ) + self.assertEqual(prompt_file.call_args.args[1]['model'], 'MiniMax-M2.7') + + +class ChatCompletionTests(unittest.TestCase): + def test_client_exposes_chat_completions(self): + client = codex_query.openai.OpenAI( + api_key='test-key', + base_url='https://api.minimax.io/v1' + ) + try: + self.assertTrue(callable(client.chat.completions.create)) + finally: + client.close() + + def test_create_chat_completion_uses_configured_model(self): + client = MagicMock() + client.chat.completions.create.return_value = SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace(content='ls -la'))] + ) + config = { + 'model': 'MiniMax-M3', + 'shell': 'bash', + 'temperature': 0, + 'max_tokens': 300 + } + + result = codex_query.create_chat_completion(client, config, '# list files') + + self.assertEqual(result, 'ls -la') + request = client.chat.completions.create.call_args.kwargs + self.assertEqual(request['model'], 'MiniMax-M3') + self.assertEqual(request['messages'][1], { + 'role': 'user', + 'content': '# list files' + }) + self.assertNotIn('stop', request) + + +class ConfigurationTests(unittest.TestCase): + def test_prompt_file_persists_model(self): + config = { + 'model': 'MiniMax-M3', + 'temperature': 0, + 'max_tokens': 300, + 'shell': 'bash', + 'multi_turn': 'off', + 'token_count': 0 + } + + with tempfile.TemporaryDirectory() as directory: + prompt_file = PromptFile.__new__(PromptFile) + prompt_file.config_path = Path(directory) / 'context.config' + prompt_file.set_config(config) + + self.assertEqual(prompt_file.read_config(), config) + + def test_set_model_command_updates_config(self): + prompt_file = MagicMock() + prompt_file.config = { + 'model': 'MiniMax-M3', + 'temperature': 0, + 'max_tokens': 300, + 'shell': 'bash', + 'multi_turn': 'off', + 'token_count': 0 + } + + with patch('builtins.print'): + result, returned_prompt_file = get_command_result( + '# set model MiniMax-M2.7', + prompt_file + ) + + self.assertEqual(result, 'config set') + self.assertIs(returned_prompt_file, prompt_file) + self.assertEqual(prompt_file.config['model'], 'MiniMax-M2.7') + prompt_file.set_config.assert_called_once_with(prompt_file.config) + + +if __name__ == '__main__': + unittest.main()