Skip to content

Commit 3054656

Browse files
committed
load token with fn/expression, closes #122
1 parent 3ed0ad9 commit 3054656

File tree

11 files changed

+88
-18
lines changed

11 files changed

+88
-18
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ END
343343
" - options.request_timeout: request timeout in seconds
344344
" - options.auth_type: API authentication method (bearer, api-key, none)
345345
" - options.token_file_path: override global token configuration
346+
" - options.token_load_fn: expression/vim function to load token
346347
" - options.selection_boundary: selection prompt wrapper (eliminates empty responses, see #20)
347348
" - ui.paste_mode: use paste mode (see more info in the Notes below)
348349
let g:vim_ai_complete = {
@@ -358,6 +359,7 @@ let g:vim_ai_complete = {
358359
\ "stream": 1,
359360
\ "auth_type": "bearer",
360361
\ "token_file_path": "",
362+
\ "token_load_fn": "",
361363
\ "selection_boundary": "#####",
362364
\ "initial_prompt": s:initial_complete_prompt,
363365
\ },
@@ -375,6 +377,7 @@ let g:vim_ai_complete = {
375377
" - options.request_timeout: request timeout in seconds
376378
" - options.auth_type: API authentication method (bearer, api-key, none)
377379
" - options.token_file_path: override global token configuration
380+
" - options.token_load_fn: expression/vim function to load token
378381
" - options.selection_boundary: selection prompt wrapper (eliminates empty responses, see #20)
379382
" - ui.paste_mode: use paste mode (see more info in the Notes below)
380383
let g:vim_ai_edit = {
@@ -390,6 +393,7 @@ let g:vim_ai_edit = {
390393
\ "stream": 1,
391394
\ "auth_type": "bearer",
392395
\ "token_file_path": "",
396+
\ "token_load_fn": "",
393397
\ "selection_boundary": "#####",
394398
\ "initial_prompt": s:initial_complete_prompt,
395399
\ },
@@ -415,6 +419,7 @@ END
415419
" - options.request_timeout: request timeout in seconds
416420
" - options.auth_type: API authentication method (bearer, api-key, none)
417421
" - options.token_file_path: override global token configuration
422+
" - options.token_load_fn: expression/vim function to load token
418423
" - options.selection_boundary: selection prompt wrapper (eliminates empty responses, see #20)
419424
" - ui.open_chat_command: preset (preset_below, preset_tab, preset_right) or a custom command
420425
" - ui.populate_options: dump [chat] config to the chat header
@@ -434,6 +439,7 @@ let g:vim_ai_chat = {
434439
\ "stream": 1,
435440
\ "auth_type": "bearer",
436441
\ "token_file_path": "",
442+
\ "token_load_fn": "",
437443
\ "selection_boundary": "",
438444
\ "initial_prompt": s:initial_chat_prompt,
439445
\ },
@@ -452,6 +458,7 @@ let g:vim_ai_chat = {
452458
" - options.request_timeout: request timeout in seconds
453459
" - options.auth_type: API authentication method (bearer, api-key, none)
454460
" - options.token_file_path: override global token configuration
461+
" - options.token_load_fn: expression/vim function to load token
455462
" - options.download_dir: path to image download directory, `cwd` if not defined
456463
let g:vim_ai_image = {
457464
\ "provider": "openai",
@@ -465,6 +472,7 @@ let g:vim_ai_image = {
465472
\ "request_timeout": 40,
466473
\ "auth_type": "bearer",
467474
\ "token_file_path": "",
475+
\ "token_load_fn": "",
468476
\ },
469477
\ "ui": {
470478
\ "download_dir": "",
@@ -477,6 +485,9 @@ let g:vim_ai_roles_config_file = s:plugin_root . "/roles-example.ini"
477485
" custom token file location
478486
let g:vim_ai_token_file_path = "~/.config/openai.token"
479487
488+
" custom fn to load token, e.g. "g:GetAIToken()"
489+
let g:vim_ai_token_load_fn = ""
490+
480491
" enables/disables full markdown highlighting in aichat files
481492
" NOTE: code syntax highlighting works out of the box without this option enabled
482493
" NOTE: highlighting may be corrupted when using together with the `preservim/vim-markdown`

autoload/vim_ai_config.vim

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ let g:vim_ai_openai_complete = {
7474
\ "stream": 1,
7575
\ "auth_type": "bearer",
7676
\ "token_file_path": "",
77+
\ "token_load_fn": "",
7778
\ "selection_boundary": "#####",
7879
\ "initial_prompt": s:initial_complete_prompt,
7980
\}
@@ -88,6 +89,7 @@ let g:vim_ai_openai_chat = {
8889
\ "stream": 1,
8990
\ "auth_type": "bearer",
9091
\ "token_file_path": "",
92+
\ "token_load_fn": "",
9193
\ "selection_boundary": "",
9294
\ "initial_prompt": s:initial_chat_prompt,
9395
\}
@@ -100,6 +102,7 @@ let g:vim_ai_openai_image = {
100102
\ "request_timeout": 40,
101103
\ "auth_type": "bearer",
102104
\ "token_file_path": "",
105+
\ "token_load_fn": "",
103106
\}
104107

105108
if !exists("g:vim_ai_open_chat_presets")
@@ -124,6 +127,9 @@ endif
124127
if !exists("g:vim_ai_token_file_path")
125128
let g:vim_ai_token_file_path = "~/.config/openai.token"
126129
endif
130+
if !exists("g:vim_ai_token_load_fn")
131+
let g:vim_ai_token_load_fn = ""
132+
endif
127133
if !exists("g:vim_ai_roles_config_file")
128134
let g:vim_ai_roles_config_file = s:plugin_root . "/roles-example.ini"
129135
endif

doc/vim-ai.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Options: >
4242
\ "stream": 1,
4343
\ "auth_type": "bearer",
4444
\ "token_file_path": "",
45+
\ "token_load_fn": "",
4546
\ "selection_boundary": "#####",
4647
\ "initial_prompt": s:initial_complete_prompt,
4748
\ },
@@ -82,6 +83,7 @@ Options: >
8283
\ "stream": 1,
8384
\ "auth_type": "bearer",
8485
\ "token_file_path": "",
86+
\ "token_load_fn": "",
8587
\ "selection_boundary": "#####",
8688
\ "initial_prompt": s:initial_complete_prompt,
8789
\ },
@@ -120,6 +122,7 @@ Options: >
120122
\ "stream": 1,
121123
\ "auth_type": "bearer",
122124
\ "token_file_path": "",
125+
\ "token_load_fn": "",
123126
\ "selection_boundary": "",
124127
\ "initial_prompt": s:initial_chat_prompt,
125128
\ },
@@ -171,6 +174,7 @@ Options: >
171174
\ "request_timeout": 40,
172175
\ "auth_type": "bearer",
173176
\ "token_file_path": "",
177+
\ "token_load_fn": "",
174178
\ },
175179
\ "ui": {
176180
\ "download_dir": "",

py/providers/openai.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def request(self, messages: Sequence[AIMessage]) -> Iterator[AIResponseChunk]:
3030
'request_timeout': options['request_timeout'],
3131
'auth_type': options['auth_type'],
3232
'token_file_path': options['token_file_path'],
33+
'token_load_fn': options['token_load_fn'],
3334
}
3435

3536
def _flatten_content(messages):
@@ -70,7 +71,11 @@ def _filter_valid_chunks(chunk):
7071
return filter(_filter_valid_chunks, map(_map_chunk, response))
7172

7273
def _load_api_key(self):
73-
raw_api_key = self.utils.load_api_key("OPENAI_API_KEY", self.options['token_file_path'])
74+
raw_api_key = self.utils.load_api_key(
75+
"OPENAI_API_KEY",
76+
token_file_path=self.options['token_file_path'],
77+
token_load_fn=self.options['token_load_fn'],
78+
)
7479
# The text is in format of "<api key>,<org id>" and the
7580
# <org id> part is optional
7681
elements = raw_api_key.strip().split(",")
@@ -118,6 +123,7 @@ def request_image(self, prompt: str) -> list[AIImageResponseChunk]:
118123
'request_timeout': options['request_timeout'],
119124
'auth_type': options['auth_type'],
120125
'token_file_path': options['token_file_path'],
126+
'token_load_fn': options['token_load_fn'],
121127
}
122128
openai_options = {
123129
'model': options['model'],

py/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def print_debug(self, text: str, *args: Any):
2222
pass
2323
def make_known_error(self, message: str):
2424
pass
25-
def load_api_key(self, env_variable: str, file_path: str):
25+
def load_api_key(self, env_variable: str, token_file_path: str = "", token_load_fn: str = ""):
2626
pass
2727

2828
class AIResponseChunk(TypedDict):

py/utils.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,44 @@ def print_debug(text, *args):
2828
class KnownError(Exception):
2929
pass
3030

31+
def load_token_from_env_variable(env_variable_name):
32+
return os.getenv(env_variable_name).strip()
33+
34+
def load_token_from_file_path(path):
35+
if not path:
36+
return None
37+
with open(os.path.expanduser(path), 'r') as file:
38+
return file.read().strip()
39+
40+
def load_token_from_fn(expression):
41+
if not expression:
42+
return None
43+
return vim.eval(expression).strip()
44+
3145
class AIProviderUtils():
3246
def print_debug(self, text, *args):
3347
print_debug(text, *args)
3448

3549
def make_known_error(self, message: str):
3650
return KnownError(message)
3751

38-
def load_api_key(self, env_variable_name: str, token_file_path: str):
39-
# token precedence: env variable, config file path, global file path
40-
api_key = os.getenv(env_variable_name)
41-
if api_key:
42-
return api_key
43-
try:
44-
global_token_file_path = vim.eval("g:vim_ai_token_file_path")
45-
token_file_path = token_file_path or global_token_file_path
46-
with open(os.path.expanduser(token_file_path), 'r') as file:
47-
api_key = file.read()
48-
except Exception:
49-
pass
50-
if not api_key:
51-
raise KnownError("Missing API key")
52-
return api_key
52+
def load_api_key(self, env_variable_name: str, token_file_path: str = "", token_load_fn: str = ""):
53+
loaders = (
54+
lambda: load_token_from_file_path(token_file_path),
55+
lambda: load_token_from_fn(token_load_fn),
56+
lambda: load_token_from_env_variable(env_variable_name),
57+
lambda: load_token_from_file_path(vim.eval("g:vim_ai_token_file_path")),
58+
lambda: load_token_from_fn(vim.eval("g:vim_ai_token_load_fn")),
59+
)
60+
for loader in loaders:
61+
try:
62+
api_key = loader()
63+
if api_key:
64+
return api_key
65+
except Exception:
66+
pass
67+
68+
raise KnownError("Missing API key")
5369

5470
ai_provider_utils = AIProviderUtils()
5571

tests/context_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"max_tokens": "0",
88
"temperature": "1",
99
"token_file_path": "",
10+
"token_load_fn": "",
1011
"selection_boundary": "",
1112
"initial_prompt": "You are a general assistant.",
1213
},
@@ -27,6 +28,7 @@
2728
"size": "1024x1024",
2829
"style": "vivid",
2930
"token_file_path": "",
31+
"token_load_fn": "",
3032
},
3133
"ui": {
3234
"paste_mode": "1",

tests/deprecated_role_syntax_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"stream": "1",
1212
"auth_type": "bearer",
1313
"token_file_path": "",
14+
"token_load_fn": "",
1415
"selection_boundary": "",
1516
"initial_prompt": "You are a general assistant.",
1617
},

tests/mocks/vim.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def eval(cmd):
1212
return os.path.abspath(os.path.join(dirname, '../..'))
1313
case 'getcwd()':
1414
return os.path.abspath(os.path.join(dirname, '../..'))
15+
case 'g:LoadToken()':
16+
return 'fn.secret'
1517
case _:
1618
return None
1719

tests/resources/example.token

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file.secret

0 commit comments

Comments
 (0)